LabHub
배우기 러닝패스 코스

Terraform in Practice

Three Services by Three Environments Became Twenty Blocks by Hand

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

중첩 객체 변수를 받아 맵으로 바꾸고, 이중 반복을 flatten 과 setproduct 두 방식으로 펼쳐 같은 결과를 내고, dynamic 블록으로 선택적 블록을 만들고 그 한계를 오류로 확인한 뒤, 펼친 행렬을 파일과 보고서로 렌더합니다.

왜 중요한가

인프라 코드가 길어지는 진짜 이유는 리소스가 많아서가 아니라 같은 모양이 반복되어서입니다. 그 반복을 손으로 펼치면 고칠 곳이 스무 군데가 되고, 리뷰하는 사람은 스무 블록이 정말 같은지 눈으로 대조해야 합니다. HCL 은 이것을 두 갈래로 풉니다 — 값을 만드는 쪽과 블록을 만드는 쪽입니다. 값 쪽(for 표현식·flatten·setproduct·객체 형)은 얼마든지 접고 펼 수 있고, 블록 쪽(dynamic)은 프로바이더 스키마가 허락하는 범위 안에서만 만들 수 있습니다. 이 차이를 모르면 dynamic 으로 모든 것을 짜려다 읽을 수 없는 설정을 만들게 됩니다. 실무에서 dynamic 이 가장 값어치 있게 쓰이는 자리는 사실 반복이 아니라 '이 블록을 넣을까 말까' 라는 선택이고, 반복은 대개 for_each 와 값 표현식이 맡습니다.

단계

  1. /root/tfa-dyn/variables.tflist(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 하세요.
  2. /root/tfa-dyn/svc.tflocal.by_name(이름을 키로 잡은 맵)을 만들고, 그것으로 for_each 를 돌려 서비스마다 /root/tfa-dyn/out/svc/<이름>.conf 를 쓰는 local_file.svc 를 선언하세요. 파일 내용은 name=<이름>port=<번호> 두 줄입니다. apply 한 뒤 상태의 인스턴스 키가 무엇인지 보세요.
  3. /root/tfa-dyn/flat.tf 에 서비스 × 그 서비스의 환경을 모두 펼친 local.pairs(각 항목은 env·name·port 를 가진 객체)를 flatten 으로 만들고, "<env>/<name>" 을 키로 잡은 local.by_key 맵을 jsonencode/root/tfa-dyn/out/flat.json 에 쓰세요. 항목은 여섯 개가 나와야 합니다.
  4. /root/tfa-dyn/product.tflocal.all_envs(모든 환경의 중복 없는 목록)를 만들고, setproduct(local.all_envs, var.services) 의 모든 조합 중 그 서비스가 실제로 쓰는 환경만 남겨 local.combo_by_key 를 만든 뒤 /root/tfa-dyn/out/product.json 에 쓰세요. 3단계 결과와 완전히 같아야 합니다.
  5. /root/tfa-dyn/cert.tfvar.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 에 저장하세요.
  6. /root/tfa-dyn/cert.tfdynamic "subject" 가 두 개의 블록을 만들도록 잠깐 고쳐 apply 하고, 실패 출력을 /root/tfa-dyn/out/limit-error.txt 에 저장하세요. 그다음 원래대로 되돌려 다시 적용하고 계획이 깨끗한 상태로 마칩니다.
  7. /root/tfa-dyn/rendered.tf 에 3단계의 local.by_keyfor_each 를 돌려 /root/tfa-dyn/rendered/<env>-<name>.conf 여섯 개를 만드세요. 내용은 env=, name=, port= 세 줄입니다.
  8. /root/tfa-dyn/summary.tf 에 환경별 항목 수(by_env), 이름을 정렬한 목록(services), 서비스 포트의 합(total_ports) 세 가지를 담은 /root/tfa-dyn/out/summary.json 을 만드세요. 세 값은 모두 HCL 표현식으로 계산합니다.

참고

중첩 객체를 변수 형으로 받는다

/root/tfa-dyn/variables.tflist(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.tflocal.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.tflocal.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.tfvar.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.tfdynamic "subject" 가 두 개의 블록을 만들도록 잠깐 고쳐 apply 하고, 실패 출력을 /root/tfa-dyn/out/limit-error.txt 에 저장하세요. 그다음 원래대로 되돌려 다시 적용하고 계획이 깨끗한 상태로 마칩니다.

dynamic 은 블록을 몇 개든 만들어 주는 장치가 아니라 '설정 값으로 블록을 만드는' 장치입니다. 몇 개까지 허용되는지는 프로바이더의 스키마가 정합니다. 오류 메시지가 허용 범위와 실제 개수를 함께 말해 주는지 보세요.

펼친 행렬을 파일로 렌더한다

/root/tfa-dyn/rendered.tf 에 3단계의 local.by_keyfor_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 함수로 구합니다. 여기서 정렬을 하는 이유는 사람이 보기 좋아서가 아니라, 정렬하지 않으면 같은 입력이 실행마다 다른 순서로 나와 비교가 불가능해지기 때문입니다.