LabHub
배우기 러닝패스 코스

Terraform実戦

サービス三つ × 環境三つを手で展開したら二十ブロックになった

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