Terraform이란?
이 포스팅은 "Terraform으로 시작하는 IaC"라는 책을 보고 정리한 내용입니다.
https://ebook-product.kyobobook.co.kr/dig/epd/ebook/E000008932854
테라폼으로 시작하는 IaC | 김민수
국내 유일 테라폼 집필서! 생성형 AI 활용으로 더 쉽고 강력해진 인프라 운영 『테라폼으로 시작하는 IaC』초판은 탄탄한 구성과 풍부한 예제를 갖춘 국내 유일한 테라폼 집필서로 각종 기업이나
ebook-product.kyobobook.co.kr
Terraform은 코드형 인프라(IaC) 도구 중 가장 인기 있는 IaC 도구입니다.
사람이 수동으로 인프라를 관리하는 것이 아니라 코드를 통해 인프라를 관리하기 때문에 다음과 같은 장점을 가지고 있습니다.
- 효율성 : 코드를 통해 인프라를 관리하기 때문에 인프라 변경이 수동으로 작업할 때보다 빠릅니다.
- 버전 관리 : 코드 형태로 관리하기 때문에 버전 관리를 할 수 있습니다.
- 협업 : 쉽게 공유가 가능합니다.
- 재사용성 : 기존 모듈을 활용하여 배포할 수 있습니다.
Terraform은 HCL(HashiCorp Configuration Language)이 코드 영역을 담당하고 있습니다.
그렇다면 지금부터 HCL 문법에 대해 알아보면서 Terraform 기본 사용법에 대해 익혀보겠습니다.
Terraform 명령어
우선 test.tf에 terraform 코드를 입력합시다.
테라폼 코드는 tf 확장자를 가집니다.
<test.tf>
resource "local_file" "abc"{
content = "abc"
filename = "abc.txt"
}
terraform init
테라폼 구성 파일이 있는 작업 디렉토리를 초기화하는데 사용합니다.
테라폼 코드를 바탕으로 필요한 프로바이더 플러그인을 찾고 설치해줍니다.
terraform init
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/local...
- Installing hashicorp/local v2.5.3...
- Installed hashicorp/local v2.5.3 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform validate
terraform 코드의 유효성을 검증합니다.
terraform validate
Success! The configuration is valid.
terraform plan
terraform으로 변경되는 인프라의 실행 계획을 생성합니다.
test.tf에서 새로 생성되는 파일에 대한 정보를 확인할 수 있습니다.
terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# local_file.abc will be created
+ resource "local_file" "abc" {
+ content = "abc"
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "abc.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
terraform apply
terraform으로 변경되는 인프라의 실행계획을 실행합니다.
terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# local_file.abc will be created
+ resource "local_file" "abc" {
+ content = "abc"
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "abc.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
local_file.abc: Creating...
local_file.abc: Creation complete after 0s [id=a9993e364706816aba3e25717850c26c9cd0d89d]
"abc"라는 내용이 담긴 abc.txt 파일이 생성된 것을 확인할 수 있습니다.
1 - terraform 블록
terraform 블록은 terraform의 구성을 명시하는데 사용됩니다.
협업할 때는 terraform 블록에 terraform 및 provider 버전을 명시적으로 선언해야 실행 오류를 줄일 수 있습니다.
terraform 블록에는 다음과 같은 블록들을 넣을 수 있습니다.
- backend 블록 : 테라폼 실행 시 저장되는 state의 저장 위치를 선언합니다. 하나의 백엔드만 허용됩니다. 민감한 정보들이 포함될 수 있기 때문에 state의 접근 제어가 필요합니다.
- cloud 블록 : v1.1 이전에는 backend의 remote 항목을 사용했지만 이후에는 cloud 블록을 사용할 수 있습니다.
다음 코드는 state의 local 저장 위치를 변경하는 terraform 코드입니다.
terraform {
required_version = "> 1.3.0" # 테라폼 버전
required_providers { # 프로바이더 버전을 나열
random = {
version = ">= 3.0.0"
}
aws = {
version = "4.2.0"
}
}
# cloud { # Cloud/Enterprise 같은 원격 실행을 위한 정보
# organization = "<MY_ORG_NAME>"
# workspaces {
# name = "my-first-workspace"
# }
# }
backend "local" { # state를 보관하는 위치를 지정
path = "./state/terraform.tfstate"
}
}
참고로 backend 블록이 변경되면 state의 위치를 재설정 해야 하는데 init 명령어에 -migrate-state 옵션을 붙여 재설정 할 수 있습니다.
terraform init -migrate-state
2 - resource 블록
resource 블록은 선언된 항목을 생성하는 동작을 수행하는 블록입니다.
리소스는 다음과 같이 선언할 수 있습니다.
resource "<프로바이더 이름>_<프로바이더 제공 리소스 유형>" "<고유한 이름>"{
<인수> = <값>
}
처음에 작성했던 test.tf 파일을 다시 한 번 봅시다.
resource "local_file" "abc"{
content = "abc"
filename = "abc.txt"
}
프로바이더가 local, 유형이 file, 고유한 이름이 abc라는 것을 알 수 있습니다.
유형들은 프로바이더에 종속성을 갖기 때문에 terraform init을 수행할 때 해당 프로바이더를 함께 설치합니다.
예를 들어 다음과 같은 resource 블록을 추가했다고 합시다.
aws EC2 인스턴스를 생성하는 resource 블록입니다.
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4" # init 테스트를 위한 임시 ami id
instance_type = "t2.micro"
}
이 상태로 terraform init 명령어를 입력하면 aws 프로바이더가 자동으로 설치됩니다.
여러 리소스 간 구성 요소 사이 종속성이 없는 경우 terraform은 병렬 실행하여 리소스를 생성하게 됩니다.
명시적으로 종속성을 부여하려면 depends_on을 사용하면 됩니다.
다음은 두 개의 file 리소스 사이 종속성을 부여하여 선후 관계를 정의한 코드입니다.
resource "local_file" "abc" {
content = "123!"
filename = "${path.module}/abc.txt"
}
resource "local_file" "def" {
depends_on = [
local_file.abc
]
content = "456!"
filename = "${path.module}/def.txt"
}
참고로 terraform graph 명령어를 통해 digraph 코드를 생성하여 리소스 간 종속 관계를 시각화 할 수 있습니다.
리소스의 기본 수명주기를 바꿀 수도 있습니다.
- create_before_destroy (bool) : 리소스 수정 시 "리소스 삭제 -> 리소스 생성" 순서를 "리소스 생성 -> 리소스 삭제"로 변경 (생성된 리소스가 삭제되지 않도록 주의)
- prevent_destroy (bool) : 리소스를 삭제할 때 거부
- ignore_changes (list) : 리소스 요소의 인수 변경 사항이 수정 계획에는 반영되지만 테라폼 실행 시 반영 X
- precondition : 리소스 요소에 선언된 인수의 조건 검증
- postcondition : plan과 apply 이후 결과를 속성 값으로 검증
variable "file_name" {
default = "step0.txt"
}
resource "local_file" "abc" {
content = "lifecycle - step 6" # 수정
filename = "${path.module}/${var.file_name}"
lifecycle {
precondition {
condition = var.file_name == "step6.txt"
error_message = "file name is not \"step6.txt\""
}
}
}
3 - data 블록
data 블록은 데이터 소스 블록으로 데이터 소스 유형을 정의합니다.
데이터 소스는 테라폼으로 정의되지 않은 외부 리소스 또는 저장된 정보를 테라폼 내에서 참조할 때 사용합니다.
리소스 블록과 유사합니다.
data "<프로바이더 이름>_<프로바이더 제공 리소스 유형>" "<고유한 이름>"{
<인수> = <값>
}
참조의 경우 resource 블록과 다음과 같은 차이가 있습니다.
resource "local_file" "abc" {
content = "123!"
filename = "./abc.txt"
}
// resource 블록 참조 : local_file.abc
data "local_file" "abc" {
filename = "./abc2.txt"
}
// data 블록 참조 : data.local_file.abc
앞에서 데이터 소스는 외부 리소스를 참조한다고 했습니다.
위 코드의 경우 data 블록에서 "./abc2.txt" 파일을 참조하고 있다는 것을 알 수 있습니다.
data.local_file.abc.content를 통해 "./abc2.txt"의 내용을 읽을 수 있습니다.
4 - variable 블록
variable 블록은 입력 변수를 사용할 때 사용합니다.
입력 변수는 인프라를 구성하는 데 필요한 속성 값을 정의해 코드의 변경 없이 여러 인프라를 생성하기 위해 사용합니다.
variable 블록은 다음과 같이 선언할 수 있습니다.
variable "<이름>" {
<인수> = <값>
}
변수 정의 할 때 다음과 같은 메타인수를 사용할 수 있습니다.
- default: 변수 기본값
- type: 변수 유형
- description: 입력 변수 설명
- validation: 제약 조건
- sensitive: 테라폼의 출력문에서 값 노출 제한
- nullable: 변수에 값이 없어도 됨
variable에는 다양한 타입들이 존재하고 다음과 같이 사용할 수 있습니다.
variable "string" {
type = string
description = "var String"
default = "myString"
}
variable "number" {
type = number
default = 123
}
variable "boolean" {
default = true
}
variable "list" {
default = [
"google",
"vmware",
"amazon",
"microsoft"
]
}
output "list_index_0" {
value = var.list.0
}
output "list_all" {
value = [
for name in var.list : upper(name)
]
}
variable "map" { # Sorting
default = {
aws = "amazon",
azure = "microsoft",
gcp = "google"
}
}
variable "set" { # Sorting
type = set(string)
default = [
"google",
"vmware",
"amazon",
"microsoft"
]
}
variable "object" {
type = object({ name = string, age = number })
default = {
name = "abc"
age = 12
}
}
variable "tuple" {
type = tuple([string, number, bool])
default = ["abc", 123, true]
}
variable "ingress_rules" { # optional ( >= terraform 1.3.0)
type = list(object({
port = number,
description = optional(string),
protocol = optional(string, "tcp"),
}))
default = [
{ port = 80, description = "web" },
{ port = 53, protocol = "udp" }]
}
이제 variable 블록을 사용하는 방법을 알아봅시다.
variable "string" {
type = string
description = "var String"
default = "myString"
}
output "variable_value" {
value = var.string
description = "입력받은 string 변수의 값"
}
위 코드를 그대로 실행하게 되면 "string" 입력 변수에는 default인 "myString" 값이 들어가게 됩니다.
입력 변수에 다른 값을 넣는 방법에 대해 알아봅시다.
1. 명령줄에서 입력
terraform apply -var="string=hello"
apply의 var 옵션에 입력 변수를 할당하면 됩니다.
2. tfvars 파일 사용
혹은 tfvars 파일을 생성해서 그 안에 값을 정의해도 됩니다.
<terraform.tfvars>
string = "tfvars 파일에서 설정한 값"
terraform apply -auto-approve
3. 환경 변수 사용
환경 변수에 값을 정의해도 됩니다.
export TF_VAR_string="환경변수에서 설정한 값" && terraform apply -auto-approve
참고로 환경 변수보다는 tfvars 파일의 우선순위가 더욱 높기 때문에 환경 변수를 적용하려면 tfvars 파일을 삭제해야 합니다.
5 - locals 블록
locals 블록은 지역 변수를 선언할 때 사용합니다.
variable 블록과 헷갈릴 수 있는데 외부에서 입력되는 variable 블록과 달리 코드 내에서 고정되어 동작하는 값을 선언할 때 사용합니다.
locals 블록은 다음과 같이 사용할 수 있습니다.
variable "prefix" {
default = "hello"
}
locals {
name = "terraform"
}
resource "local_file" "abc" {
content = local.name
filename = "${path.module}/abc.txt"
}
6 - output 블록
output 블록은 프로비저닝 수행 후 결과 속성 값을 확인하는 용도로 사용합니다.
output 블록은 다음과 같이 사용할 수 있습니다.
output "test" {
value = "hello world!"
}
value에 존재하는 값이 출력되게 됩니다.
다음과 같은 메타 인수를 활용할 수 있습니다.
- description: 출력 값 설명
- sensitive: 출력에서 값 노출 제한
- depends_on: 순서 조정
- precondition: 출력 전에 조건 검증
7 - count 블록
count를 사용하면 리소스나 모듈 블록을 반복적으로 생성할 수 있습니다.
여러 데이터가 있는 variable을 선언하고 count.index를 통해 접근할 수 있습니다.
아래 코드는 local_file 리소스를 여러 번 생성하는 코드입니다.
variable "names" {
type = list(string)
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
count = length(var.names)
content = "abc"
# 변수 인덱스에 직접 접근
filename = "${path.module}/abc-${var.names[count.index]}.txt"
}
8 - for_each 블록
map 또는 set 형태의 데이터에서 key 개수만큼 리소스를 생성할 수 있습니다.
each.key를 통해 key에 접근할 수 있고 each.value를 통해 value에 접근할 수 있습니다.
variable "names" {
default = {
a = "content a"
b = "content b"
c = "content c"
}
}
resource "local_file" "abc" {
for_each = var.names
content = each.value
filename = "${path.module}/abc-${each.key}.txt"
}
9 - dynamic 블록
count나 for-each처럼 리소스 전체를 여러 개 생성하는 것이 아닌 리소스 내 구성 블록을 다중으로 작성할 때 사용합니다.
dynamic 블록을 사용하지 않을 때 코드는 다음과 같습니다.
resource "local_file" "without_dynamic" {
content = "파일1"
filename = "file1.txt"
}
resource "local_file" "without_dynamic2" {
content = "파일2"
filename = "file2.txt"
}
variable "file_list" {
type = list(string)
default = ["파일1", "파일2", "파일3"]
}
resource "local_file" "with_dynamic" {
count = length(var.file_list)
content = var.file_list[count.index]
filename = "dynamic_file_${count.index + 1}.txt"
}
예를 들어 AWS 보안 그룹의 여러 개의 인바운드 규칙을 생성할 때 dynamic 블록을 활용할 수 있습니다.
다음은 dynamic 블록을 활용하지 않을 때의 코드입니다.
# 반복되는 ingress 규칙을 하드코딩
resource "aws_security_group" "example" {
name = "example-sg"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
}
다음은 dynamic 블록을 사용할 때의 코드입니다.
# 변수로 규칙 정의
variable "ingress_rules" {
default = [
{
port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
]
}
# dynamic 블록으로 반복 생성
resource "aws_security_group" "example" {
name = "example-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
10 - provisioner 블록
프로비저너는 프로바이더로 실행되지 않는 명령어를 실행하고 싶을 때 사용합니다.
테라폼의 상태 파일과 동기화되지 않기 때문에 프로비저닝 결과가 매번 다를 수 있습니다.
프로비저너의 사용은 최소화하는 것이 좋습니다.
다음 코드는 local file을 생성한 뒤 content를 출력하는 명령어를 실행하는 코드입니다.
variable "content"{
default = "Hello, World!"
sensitive = true
}
resource "local_file" "abc"{
filename = "abc.txt"
content = var.content
provisioner "local-exec" {
command = "echo ${var.content}"
}
}
프로비저너는 리소스 블록 작업이 종료된 후 실행되고 여러 개 프로비저너를 선언하면 순서대로 처리됩니다.
self 값을 통해 리소스 내부 값에 접근할 수도 있습니다.
resource "local_file" "abc"{
filename = "abc.txt"
content = "Hello, World!"
provisioner "local-exec" {
command = "echo ${self.filename}"
}
}
on_failure 값을 continue로 넣으면 command가 실패하더라도 다음 작업을 수행하도록 할 수 있습니다.
프로비저너의 종류는 다음과 같은 것들이 있습니다.
- local-exec: 테라폼 실행 환경에서 수행할 커맨드를 정의합니다. 내부 connection 블록으로 원격 환경에 수행할 커맨드를 정의할 수도 있습니다. (bastion-host도 가능)
- remote-exec: 원격 환경에서 실행할 커맨드와 스크립트를 정의합니다.
- file: 테라폼 실행 환경에서 원하는 곳으로 파일 또는 디렉토리를 복사할 때 사용합니다.
11 - null_resource 블록
null_resource는 아무 작업도 수행하지 않는 리소스로 주로 프로비저닝 동작을 조율할 때 사용합니다.
설명만 들으면 이해하기 어렵기 때문에 다음과 같은 코드를 보면서 이해해봅시다.
# 생성
resource "aws_instance" "foo" {
ami = "ami-5189a661"
instance_type = "t3.micro"
private_ip = "10.0.0.12"
subnet_id = aws_subnet.tf_test_subnet.id
provisioner "remote-exec" {
inline = [
"echo ${aws_eip.bar.public_ip}"
]
}
}
resource "aws_eip" "bar" {
vpc = true
instance = aws_instance.foo.id
associate_with_private_ip = aws_instance.foo.private_ip
depends_on = [aws_internet_gateway.gw]
}
보면 인스턴스 리소스를 생성한 뒤 aws_eip의 public_id를 출력하는 프로비저너 블록이 작성되어 있습니다.
즉, aws_eip 리소스에 종속성이 존재하는 상황입니다.
그리고 aws_eip의 경우 aws_instance 리소스에 종속성이 존재하는 것을 확인할 수 있습니다.
즉, 두 리소스 간 상호 종속이 발생한 상황입니다.
이 상태에서 terraform graph 명령어를 실행해볼까요?
╷
│ Error: Cycle: aws_instance.foo (expand), aws_eip.bar (expand)
│
│
╵
싸이클이 존재한다는 에러가 발생했습니다.
프로바이더가 제공하는 리소스 수명 주기만으로는 이러한 문제를 해결하기 어렵기 때문에 null_resource를 활용할 수 있습니다.
aws_instance의 aws_eip에 대한 종속을 끊고 프로비저닝 코드를 null_resource 블록으로 옮기면 됩니다.
resource "aws_instance" "foo" {
ami = "ami-5189a661"
instance_type = "t3.micro"
private_ip = "10.0.0.12"
subnet_id = aws_subnet.tf_test_subnet.id
}
resource "aws_eip" "bar" {
instance = aws_instance.foo.id
associate_with_private_ip = "10.0.0.12"
depends_on = [aws_internet_gateway.gw]
}
resource "null_resource" "bar2" {
provisioner "remote-exec" {
connection {
host = aws_eip.bar.public_ip
# 추가 연결 설정 필요 (type, user, private_key 등)
}
inline = [
"echo ${aws_eip.bar.public_ip}"
]
}
}
12 - terraform_data 블록
terraform_data는 null_resource처럼 아무 작업도 수행하지 않는 리소스입니다.
null_resource는 별도로 프로바이더 구성을 해야 하지만 terraform_data는 프로바이더 구성 없이 기본 수명주기 관리자가 제공되는 것이 장점입니다.
다음과 같이 사용할 수 있습니다.
resource "terraform_data" "foo" {
triggers_replace = {
aws_instance_bar_id,
aws_instance_barz_id
}
input = "world"
}
output "terraform_data_output" {
value = terraform_data.foo.output # 출력 결과는 "world"
}
13 - moved 블록
terraform state의 리소스 주소 이름이 변경되면 기존 리소스는 삭제되고 새로운 리소스가 생성됩니다.
moved 블록을 활용하면 리소스 영향 없이 주소를 변경할 수 있습니다.
아래 코드의 local_file 이름을 b로 바꾼다면 원래는 리소스 삭제 후 생성되어야 합니다.
resource "local_file" "a" {
content = "a"
filename = "${path.module}/a.txt"
}
output "file_content" {
value = local_file.a.content
}
위 코드를 이름만 바꾸고 terraform plan을 실행하면 리소스 하나가 삭제되고 하나가 생성된다고 나옵니다.
resource "local_file" "b" {
content = "a"
filename = "${path.module}/a.txt"
}
output "file_content" {
value = local_file.b.content
}
Plan: 1 to add, 0 to change, 1 to destroy.
그러면 이제 리소스 영향을 주지 않고 moved 블록을 통해 리소스 주소를 변경해봅시다.
resource "local_file" "b" {
content = "a"
filename = "${path.module}/a.txt"
}
output "file_content" {
value = local_file.b.content
}
moved {
from = local_file.a
to = local_file.b
}
다시 terraform plan을 실행하면 생성되는 리소스와 제거되는 리소스도 없다는 것을 알 수 있습니다.
Terraform will perform the following actions:
# local_file.a has moved to local_file.b
resource "local_file" "b" {
id = "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8"
# (10 unchanged attributes hidden)
}
Plan: 0 to add, 0 to change, 0 to destroy.
조건식
terraform은 삼항 연산자를 지원하고 있습니다.
count와 삼항 연산자를 활용하면 리소스 생성 여부를 결정할 수 있습니다.
variable "enable_file" {
default = true
}
resource "local_file" "foo" {
count = var.enable_file ? 1 : 0
content = "foo!"
filename = "${path.module}/foo.bar"
}
output "content" {
value = var.enable_file ? local_file.foo[0].content : ""
}
출처
https://github.com/terraform101/terraform-basic
GitHub - terraform101/terraform-basic: [Chapter 3] Terraform Basic
[Chapter 3] Terraform Basic. Contribute to terraform101/terraform-basic development by creating an account on GitHub.
github.com