Terraform (2) - 변수, 출력, 반복으로 여러 리소스 다루기
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을 입력하면 실제 값을 바로 확인할 수 있다.