サービス三つ × 環境三つを手で展開したら二十ブロックになった
한국어 원문으로 표시합니다.
목표
중첩 객체 변수를 받아 맵으로 바꾸고, 이중 반복을 flatten 과 setproduct 두 방식으로 펼쳐 같은 결과를 내고, dynamic 블록으로 선택적 블록을 만들고 그 한계를 오류로 확인한 뒤, 펼친 행렬을 파일과 보고서로 렌더합니다.
왜 중요한가
인프라 코드가 길어지는 진짜 이유는 리소스가 많아서가 아니라 같은 모양이 반복되어서입니다. 그 반복을 손으로 펼치면 고칠 곳이 스무 군데가 되고, 리뷰하는 사람은 스무 블록이 정말 같은지 눈으로 대조해야 합니다. HCL 은 이것을 두 갈래로 풉니다 — 값을 만드는 쪽과 블록을 만드는 쪽입니다. 값 쪽(for 표현식·flatten·setproduct·객체 형)은 얼마든지 접고 펼 수 있고, 블록 쪽(dynamic)은 프로바이더 스키마가 허락하는 범위 안에서만 만들 수 있습니다. 이 차이를 모르면 dynamic 으로 모든 것을 짜려다 읽을 수 없는 설정을 만들게 됩니다. 실무에서 dynamic 이 가장 값어치 있게 쓰이는 자리는 사실 반복이 아니라 '이 블록을 넣을까 말까' 라는 선택이고, 반복은 대개 for_each 와 값 표현식이 맡습니다.
단계
/root/tfa-dyn/variables.tf에list(object({ name = string, port = number, envs = list(string) }))형의services변수를 선언하고, 이름이 겹치지 않는지 보는validation을 붙이세요./root/tfa-dyn/terraform.tfvars에는 api(8080, dev·prod), web(8081, dev), batch(9090, dev·stage·prod) 세 개를 적습니다./root/tfa-dyn/main.tf에는 프로바이더 선언과jsonencode(var.services)를out/services.json에 쓰는local_file.services를 두고 init·apply 하세요./root/tfa-dyn/svc.tf에local.by_name(이름을 키로 잡은 맵)을 만들고, 그것으로for_each를 돌려 서비스마다/root/tfa-dyn/out/svc/<이름>.conf를 쓰는local_file.svc를 선언하세요. 파일 내용은name=<이름>과port=<번호>두 줄입니다. apply 한 뒤 상태의 인스턴스 키가 무엇인지 보세요./root/tfa-dyn/flat.tf에 서비스 × 그 서비스의 환경을 모두 펼친local.pairs(각 항목은env·name·port를 가진 객체)를flatten으로 만들고,"<env>/<name>"을 키로 잡은local.by_key맵을jsonencode해/root/tfa-dyn/out/flat.json에 쓰세요. 항목은 여섯 개가 나와야 합니다./root/tfa-dyn/product.tf에local.all_envs(모든 환경의 중복 없는 목록)를 만들고,setproduct(local.all_envs, var.services)의 모든 조합 중 그 서비스가 실제로 쓰는 환경만 남겨local.combo_by_key를 만든 뒤/root/tfa-dyn/out/product.json에 쓰세요. 3단계 결과와 완전히 같아야 합니다./root/tfa-dyn/cert.tf에var.with_subject(bool, 기본 true)와 ED25519 개인키, 그리고dynamic "subject"로 주체 블록을 켜고 끄는tls_self_signed_cert.c를 두세요. 주체는common_name = "api.internal",organization = "LabHub"입니다. 인증서는/root/tfa-dyn/out/cert.pem에 씁니다. 먼저-var with_subject=false로 적용해openssl x509 -noout -subject결과를/root/tfa-dyn/out/no-subject.txt에 저장하고, 그다음 기본값으로 다시 적용해 같은 명령의 결과를/root/tfa-dyn/out/subject.txt에 저장하세요./root/tfa-dyn/cert.tf의dynamic "subject"가 두 개의 블록을 만들도록 잠깐 고쳐 apply 하고, 실패 출력을/root/tfa-dyn/out/limit-error.txt에 저장하세요. 그다음 원래대로 되돌려 다시 적용하고 계획이 깨끗한 상태로 마칩니다./root/tfa-dyn/rendered.tf에 3단계의local.by_key로for_each를 돌려/root/tfa-dyn/rendered/<env>-<name>.conf여섯 개를 만드세요. 내용은env=,name=,port=세 줄입니다./root/tfa-dyn/summary.tf에 환경별 항목 수(by_env), 이름을 정렬한 목록(services), 서비스 포트의 합(total_ports) 세 가지를 담은/root/tfa-dyn/out/summary.json을 만드세요. 세 값은 모두 HCL 표현식으로 계산합니다.
참고
- 파드에는 OpenTofu 1.9.0 과 local·tls 프로바이더 미러가 있어 인터넷 없이 돕니다.
- 각 단계는 새 .tf 파일을 더하는 방식입니다. 앞 단계 파일을 지우지 마세요 — 뒤 단계가 그 locals 를 씁니다.
- 흔한 실수: for_each 에 리스트를 그대로 주는 것. 집합이나 맵이어야 하고, 리스트는 for 표현식으로 맵으로 바꿉니다.
- 흔한 실수: jsonencode 결과를 사람이 읽으려고 그대로 여는 것. jq 로 정렬해 보면 두 방식의 결과를 바로 견줄 수 있습니다.
- dynamic Blocks · flatten · setproduct · tls_self_signed_cert
중첩 객체를 변수 형으로 받는다
/root/tfa-dyn/variables.tf 에 list(object({ name = string, port = number, envs = list(string) })) 형의 services 변수를 선언하고, 이름이 겹치지 않는지 보는 validation 을 붙이세요. /root/tfa-dyn/terraform.tfvars 에는 api(8080, dev·prod), web(8081, dev), batch(9090, dev·stage·prod) 세 개를 적습니다. /root/tfa-dyn/main.tf 에는 프로바이더 선언과 jsonencode(var.services) 를 out/services.json 에 쓰는 local_file.services 를 두고 init·apply 하세요.
형을 object 로 적어 두면 tfvars 에서 철자가 틀렸을 때 apply 전에 막힙니다. validation 의 condition 은 참일 때 통과입니다 — 이름 개수와 중복 제거한 개수가 같은지 보면 됩니다. jsonencode 는 HCL 값을 그대로 JSON 으로 바꿔 줍니다.
리스트를 맵으로 바꿔 for_each 에 넘긴다
/root/tfa-dyn/svc.tf 에 local.by_name(이름을 키로 잡은 맵)을 만들고, 그것으로 for_each 를 돌려 서비스마다 /root/tfa-dyn/out/svc/<이름>.conf 를 쓰는 local_file.svc 를 선언하세요. 파일 내용은 name=<이름> 과 port=<번호> 두 줄입니다. apply 한 뒤 상태의 인스턴스 키가 무엇인지 보세요.
for_each 는 리스트를 받지 않습니다 — 집합이나 맵이어야 합니다. 리스트로 count 를 쓰면 가운데 항목을 지웠을 때 뒤가 전부 밀려 다시 만들어지는데, 맵 키를 쓰면 그 항목만 지워집니다. 상태 주소가 대괄호 안 키로 적히는 것을 확인하세요.
이중 반복을 flatten 으로 한 겹으로 편다
/root/tfa-dyn/flat.tf 에 서비스 × 그 서비스의 환경을 모두 펼친 local.pairs(각 항목은 env·name·port 를 가진 객체)를 flatten 으로 만들고, "<env>/<name>" 을 키로 잡은 local.by_key 맵을 jsonencode 해 /root/tfa-dyn/out/flat.json 에 쓰세요. 항목은 여섯 개가 나와야 합니다.
안쪽 for 가 리스트를 돌려주기 때문에 바깥 for 의 결과는 리스트의 리스트가 됩니다. flatten 이 그 한 겹을 벗깁니다. 맵으로 바꿔 두면 다음 단계에서 for_each 에 그대로 넘길 수 있고, 키가 곧 사람이 읽는 이름이 됩니다.
같은 행렬을 setproduct 로 다시 만든다
/root/tfa-dyn/product.tf 에 local.all_envs(모든 환경의 중복 없는 목록)를 만들고, setproduct(local.all_envs, var.services) 의 모든 조합 중 그 서비스가 실제로 쓰는 환경만 남겨 local.combo_by_key 를 만든 뒤 /root/tfa-dyn/out/product.json 에 쓰세요. 3단계 결과와 완전히 같아야 합니다.
setproduct 는 걸러 주지 않습니다 — 모든 조합을 만든 다음 for 표현식의 if 로 남길 것을 고릅니다. 조합을 다 만들었다가 버리는 방식과, 처음부터 필요한 것만 펴는 방식 중 무엇이 읽기 쉬운지 두 파일을 나란히 놓고 판단해 보세요.
dynamic 으로 블록을 있다·없다로 만든다
/root/tfa-dyn/cert.tf 에 var.with_subject(bool, 기본 true)와 ED25519 개인키, 그리고 dynamic "subject" 로 주체 블록을 켜고 끄는 tls_self_signed_cert.c 를 두세요. 주체는 common_name = "api.internal", organization = "LabHub" 입니다. 인증서는 /root/tfa-dyn/out/cert.pem 에 씁니다. 먼저 -var with_subject=false 로 적용해 openssl x509 -noout -subject 결과를 /root/tfa-dyn/out/no-subject.txt 에 저장하고, 그다음 기본값으로 다시 적용해 같은 명령의 결과를 /root/tfa-dyn/out/subject.txt 에 저장하세요.
dynamic 의 for_each 에 빈 컬렉션을 주면 그 블록이 아예 만들어지지 않습니다. 이것이 실무에서 dynamic 을 쓰는 가장 흔한 이유입니다 — 반복이 아니라 선택적 블록입니다. 인증서의 주체가 정말 비었는지는 설정이 아니라 만들어진 인증서를 열어 확인하세요.
dynamic 은 스키마가 허락하는 만큼만 만든다
/root/tfa-dyn/cert.tf 의 dynamic "subject" 가 두 개의 블록을 만들도록 잠깐 고쳐 apply 하고, 실패 출력을 /root/tfa-dyn/out/limit-error.txt 에 저장하세요. 그다음 원래대로 되돌려 다시 적용하고 계획이 깨끗한 상태로 마칩니다.
dynamic 은 블록을 몇 개든 만들어 주는 장치가 아니라 '설정 값으로 블록을 만드는' 장치입니다. 몇 개까지 허용되는지는 프로바이더의 스키마가 정합니다. 오류 메시지가 허용 범위와 실제 개수를 함께 말해 주는지 보세요.
펼친 행렬을 파일로 렌더한다
/root/tfa-dyn/rendered.tf 에 3단계의 local.by_key 로 for_each 를 돌려 /root/tfa-dyn/rendered/<env>-<name>.conf 여섯 개를 만드세요. 내용은 env=, name=, port= 세 줄입니다.
키가 이미 환경과 이름을 담고 있으니 각 파일 이름은 each.value 에서 꺼내 조립하면 됩니다. 나중에 서비스 하나가 환경 하나를 더 쓰게 되면 tfvars 한 줄만 고쳐도 파일이 하나 더 생기는지 확인해 보세요 — 그게 이 구조를 쓰는 이유입니다.
펼친 값을 다시 접어 보고서로 만든다
/root/tfa-dyn/summary.tf 에 환경별 항목 수(by_env), 이름을 정렬한 목록(services), 서비스 포트의 합(total_ports) 세 가지를 담은 /root/tfa-dyn/out/summary.json 을 만드세요. 세 값은 모두 HCL 표현식으로 계산합니다.
by_env 는 환경마다 pairs 를 걸러 길이를 세면 되고, 포트 합은 sum 함수로 구합니다. 여기서 정렬을 하는 이유는 사람이 보기 좋아서가 아니라, 정렬하지 않으면 같은 입력이 실행마다 다른 순서로 나와 비교가 불가능해지기 때문입니다.