1. 개요

이전 글에서는 파일 하나를 만드는 코드로 init, plan, apply, destroy 흐름을 익혔다. 그런데 그 코드는 파일 이름과 내용이 코드에 고정돼 있어서, 파일을 10개 만들려면 resource 블록을 10번 복사해야 한다.

이 글에서는 같은 예제를 네 가지 문법으로 바꿔 본다.

문법 하는 일
variable 값을 코드 밖에서 받는다
locals 코드 안에서 반복해 쓰는 값을 한곳에 둔다
for_each 목록의 원소마다 리소스를 하나씩 만든다
output 실행 결과를 화면에 보여 준다

예제는 이름 목록을 받아 사람마다 인사 파일을 하나씩 만들고, 만든 파일 목록을 index.txt에 적는다.


2. 파일 나누기

새 디렉터리를 만들고 파일 네 개를 둔다.

terraform-practice2/
├── versions.tf     # 필요한 provider
├── variables.tf    # 입력값 정의
├── main.tf         # 리소스
└── outputs.tf      # 출력값

Terraform은 한 디렉터리 안의 .tf 파일을 모두 합쳐서 하나로 읽는다. 파일을 나누는 것은 사람이 찾기 쉽게 하려는 관례이고, 파일 이름에 특별한 의미는 없다. 네 파일의 내용을 전부 main.tf 하나에 적어도 똑같이 동작한다.

versions.tf는 1편과 같다.

# versions.tf
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

3. variable: 값을 밖에서 받기

variable 블록은 코드 밖에서 바꿀 수 있는 입력값을 정의한다.

# variables.tf
variable "greeting" {
  description = "파일에 쓸 인사말"     # 설명 (선택)
  type        = string                # 받을 값의 타입
  default     = "Hello"               # 값을 주지 않았을 때 쓸 기본값
}

variable "names" {
  description = "인사 파일을 만들 사람 이름 목록"
  type        = set(string)           # 문자열 집합 (순서 없음, 중복 없음)
  default     = ["alice", "bob"]
}

코드에서는 var.이름으로 변수 값을 읽는다. 예를 들어 var.greeting은 "Hello"다.

type에 자주 쓰는 타입은 다음과 같다.

타입 모양 설명
string "Hello" 문자열
number 3 숫자
bool true 참/거짓
list(string) ["a", "b"] 순서가 있는 목록. 중복 가능
set(string) ["a", "b"] 순서가 없는 집합. 중복 불가
map(string) { a = "1" } 키로 값을 찾는 목록

names를 list가 아니라 set으로 정한 이유는 5장의 for_each가 set이나 map만 받기 때문이다.

3.1 변수 값을 바꾸는 방법

default 말고 다른 값을 쓰는 방법은 두 가지다.

terraform.tfvars 파일: 같은 디렉터리에 이 이름으로 파일을 두면 plan과 apply가 자동으로 읽는다. 자주 바꾸는 값을 여기에 적는다.

# terraform.tfvars
names = ["alice", "bob", "carol"]

-var 옵션: 실행할 때 한 번만 값을 바꾼다.

terraform plan -var 'greeting=Hi'

셋이 모두 있으면 -var → terraform.tfvars → default 순서로 앞쪽이 우선한다.


4. locals: 코드 안의 이름 붙은 값

locals는 코드 안에서 여러 번 쓰는 값에 이름을 붙여 두는 블록이다. 변수와 달리 밖에서 바꿀 수 없다.

# main.tf
locals {
  output_dir = "${path.module}/greetings"   # 파일을 만들 디렉터리
}

코드에서는 local.output_dir로 읽는다. 블록 이름은 locals(복수)지만 읽을 때는 local.(단수)이라는 점이 헷갈리기 쉽다.

"${...}"는 문자열 안에 값을 끼워 넣는 문법이다. path.module이 .이면 local.output_dir은 "./greetings"가 된다.


5. for_each: 목록만큼 리소스 만들기

for_each에 set이나 map을 주면 원소마다 리소스가 하나씩 만들어진다.

# main.tf (이어서)
resource "local_file" "greeting" {
  for_each = var.names                               # ["alice", "bob"] → 리소스 2개

  filename = "${local.output_dir}/${each.key}.txt"   # ./greetings/alice.txt, ./greetings/bob.txt
  content  = "${var.greeting}, ${each.key}!\n"       # "Hello, alice!", "Hello, bob!"
}

for_each 안에서는 each로 지금 만드는 원소를 가리킨다.

  • each.key: 지금 원소의 키. set이면 원소 값 자체다("alice").
  • each.value: 지금 원소의 값. set이면 each.key와 같고, map이면 키에 해당하는 값이다.

for_each로 만든 리소스는 키로 구분된다. 그래서 plan과 state에 local_file.greeting["alice"]처럼 표시되고, 코드에서도 같은 모양으로 하나씩 가리킬 수 있다.


6. 참조와 만드는 순서

이번에는 만든 인사 파일 목록을 index.txt에 적는다.

# main.tf (이어서)
resource "local_file" "index" {
  filename = "${local.output_dir}/index.txt"
  # local_file.greeting의 원소마다 "이름 -> 경로" 한 줄씩 만들어 이어 붙인다
  content  = join("", [for name, file in local_file.greeting : "${name} -> ${file.filename}\n"])
}
  • [for 키, 값 in 컬렉션 : 결과]는 for 표현식이다. 컬렉션의 원소마다 결과를 계산해 새 list를 만든다. local_file.greeting은 { "alice" = 리소스, "bob" = 리소스 } 모양이라서, name에는 "alice", file에는 그 리소스가 들어간다.
  • join("", list)는 list의 문자열을 구분자 없이 이어 붙이는 내장 함수다.

여기서 중요한 점은 index가 local_file.greeting을 참조한다는 것이다. Terraform은 코드 전체의 참조를 읽어서 “index를 만들려면 greeting이 먼저 있어야 한다”는 순서를 스스로 정한다. 코드에 순서를 따로 적지 않아도 된다. apply 로그를 보면 이 순서가 그대로 드러난다.

local_file.greeting["alice"]: Creating...
local_file.greeting["bob"]: Creating...
local_file.greeting["bob"]: Creation complete after 0s
local_file.greeting["alice"]: Creation complete after 0s
local_file.index: Creating...
local_file.index: Creation complete after 0s
  • alice와 bob은 서로 참조하지 않아서 동시에 만들어진다.
  • index는 두 파일을 참조하므로 둘이 끝난 다음에 만들어진다.

참조 없이 순서만 정해야 하는 특별한 경우에는 depends_on = [리소스]로 직접 적을 수 있다. 대부분은 참조만으로 충분하다.


7. output: 결과 보여 주기

output 블록은 apply가 끝난 뒤 화면에 보여 줄 값을 정한다.

# outputs.tf
output "greeting_files" {
  description = "만들어진 인사 파일 경로"
  value       = [for file in local_file.greeting : file.filename]
}

apply가 끝나면 마지막에 출력된다.

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

greeting_files = [
  "./greetings/alice.txt",
  "./greetings/bob.txt",
]

나중에 다시 보고 싶으면 terraform output을 실행한다. 실제 인프라에서는 만든 서버의 IP 주소처럼 “만들어 봐야 아는 값”을 확인할 때 주로 쓴다.

만들어진 파일은 다음과 같다.

cat greetings/alice.txt
# Hello, alice!

cat greetings/index.txt
# alice -> ./greetings/alice.txt
# bob -> ./greetings/bob.txt

8. 목록을 바꿔 보기

이제 변수만 바꿔서 리소스 수를 조절해 본다.

8.1 한 명 추가

terraform.tfvars에 carol을 추가한다.

# terraform.tfvars
names = ["alice", "bob", "carol"]
terraform plan
  # local_file.greeting["carol"] will be created
  # local_file.index must be replaced
Plan: 2 to add, 0 to change, 1 to destroy.

carol 파일만 새로 만들고, alice와 bob은 건드리지 않는다. index.txt는 내용이 바뀌므로 다시 만든다. resource 블록을 복사하지 않고 목록 한 줄만 바꿔서 리소스를 늘렸다.

8.2 한 명 제거

apply한 뒤 이번에는 bob을 뺀다.

names = ["alice", "carol"]
  # local_file.greeting["bob"] will be destroyed
  # (because key ["bob"] is not in for_each map)
  # local_file.index must be replaced
Plan: 1 to add, 0 to change, 2 to destroy.

키가 사라진 bob만 지운다. 리소스를 키(이름)로 구분하기 때문에, 목록 중간의 원소를 빼도 나머지는 영향을 받지 않는다.

8.3 count와의 차이

리소스를 여러 개 만드는 문법으로 count = 3도 있다. count는 리소스를 [0], [1], [2] 번호로 구분한다. 그래서 목록 중간의 원소를 빼면 뒤쪽 번호가 한 칸씩 당겨지고, 관계없는 리소스까지 교체될 수 있다. 원소마다 이름이 있는 경우에는 for_each가 안전하다.


9. 정리

variable로 값을 밖에서 받고, locals로 코드 안의 반복되는 값을 모으고, output으로 결과를 보여 준다. 값은 var., local., 리소스종류.이름.속성으로 참조하고, Terraform은 이 참조를 따라 만드는 순서를 스스로 정한다. 서로 참조하지 않는 리소스는 동시에 만들어진다.

for_each는 목록의 원소마다 리소스를 만들고, 각 리소스를 키로 구분한다. 덕분에 목록 한 줄만 바꿔도 필요한 리소스만 정확히 추가하거나 지울 수 있다.

값이 헷갈릴 때는 terraform console을 실행해 var.names나 local.output_dir을 입력하면 실제 값을 바로 확인할 수 있다.