테라폼 기초 강좌 #6 의존성과 반복: depends_on, count vs for_each, dynamic 블록
리소스가 두세 개일 때는 생각할 필요가 없던 두 가지 질문이 인프라가 커지면 나타납니다. 서로 의존하는 리소스들은 어떤 순서로 만들어지는가, 그리고 거의 같은 리소스 열 개를 열 번 복사해 적어야 하는가입니다. 이번 글에서는 테라폼이 순서를 결정하는 원리와, 반복을 없애는 세 가지 문법인 count, for_each, dynamic 블록을 다루겠습니다. 특히 count에는 실무에서 리소스를 날려 먹는 유명한 함정이 있어서, 그 동작을 #5에서 배운 state와 연결해 이해하는 것이 이번 글의 중심입니다.
참조가 곧 의존성입니다 #
#2에서 버저닝 리소스가 aws_s3_bucket.hello.id를 참조했습니다. 이 참조는 값을 가져오는 문법이면서 동시에 순서 선언입니다. 버킷이 있어야 그 id를 알 수 있으니, 테라폼은 버킷을 먼저 만들고 버저닝을 나중에 만듭니다. 코드 전체의 참조를 모으면 방향 그래프가 나오고, 테라폼은 이 그래프를 따라 실행하되 의존 관계가 없는 리소스들은 병렬로 만듭니다. 순서를 지정하는 별도 문법 없이, 값을 참조로 연결하는 좋은 습관만으로 순서 문제가 저절로 풀리는 구조입니다.
depends_on: 참조 없는 의존성의 예외 처리 #
드물게 참조는 없는데 순서는 필요한 경우가 있습니다. 전형적인 예가 IAM 권한입니다. EC2 인스턴스가 부팅 스크립트에서 S3에 접근하는데, 그 권한을 주는 정책 연결 리소스와 인스턴스 사이에는 아무 참조가 없다고 합시다. 테라폼은 둘을 병렬로 만들고, 인스턴스가 먼저 떠서 권한 없이 S3 접근을 시도하다 실패할 수 있습니다. 이럴 때 명시적으로 순서를 강제합니다.
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
depends_on = [aws_iam_role_policy_attachment.web_s3]
}주의할 것은 depends_on이 최후 수단이라는 점입니다. 참조로 표현할 수 있는 의존성을 depends_on으로 적으면 그래프가 코드에서 안 보이게 되고, 불필요한 depends_on은 병렬성을 줄여 apply만 느리게 만듭니다. 참조가 가능하면 참조로, 정말 불가능할 때만 depends_on으로 적습니다.
count: 개수로 복제하기 #
같은 리소스를 여러 개 만들 때 첫 번째 도구는 count입니다.
resource "aws_instance" "web" {
count = 3
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
tags = { Name = "web-${count.index}" }
}리소스 주소가 aws_instance.web[0], [1], [2]로 늘어나고, 블록 안에서는 count.index로 자기 번호를 쓸 수 있습니다. #4의 조건식과 조합한 조건부 생성도 count의 몫입니다.
count = var.env == "prod" ? 1 : 0운영 환경에만 만드는 리소스를 이 한 줄로 표현하는 것은 널리 쓰이는 관용구입니다.
count의 함정: 번호가 밀리면 재생성됩니다 #
count로 만든 리소스의 정체는 state에 번호로 기록됩니다. 여기서 함정이 나옵니다. 리스트 기반으로 서브넷 세 개를 만들었다고 합시다.
variable "azs" {
default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}
resource "aws_subnet" "public" {
count = length(var.azs)
availability_zone = var.azs[count.index]
# ...
}여기서 리스트 중간의 2b를 빼면 어떻게 될까요? 2c가 인덱스 2에서 1로 밀립니다. 테라폼이 보기에 [1]은 2b였는데 이제 2c가 되어야 하니 교체 대상이고, [2]는 사라졌으니 삭제 대상입니다. 의도는 “2b 하나만 삭제"였는데 plan은 2c까지 지웠다 다시 만들겠다고 제안합니다. 그 서브넷 위에 무언가 살고 있었다면 사고입니다. 원인은 리소스의 정체성이 내용이 아니라 순번에 묶여 있다는 것입니다.
for_each: 키로 복제하기 #
이 함정의 답이 for_each입니다. 번호 대신 맵의 키 또는 셋의 값이 정체성이 됩니다.
variable "azs" {
default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}
resource "aws_subnet" "public" {
for_each = toset(var.azs)
availability_zone = each.value
# ...
}주소가 aws_subnet.public["ap-northeast-2a"]처럼 키로 기록되므로, 2b를 빼면 정확히 ["ap-northeast-2b"]만 삭제 대상이 되고 나머지는 건드리지 않습니다. 맵을 넘기면 each.key와 each.value를 함께 쓸 수 있어 이름과 설정을 짝지을 때 편합니다. 리스트는 그대로 못 넘기고 toset()으로 감싸야 한다는 것만 기억해 두면 됩니다.
선택 기준을 정리하면 이렇습니다. 완전히 동일한 것을 N개 만들거나 조건부 생성(0 또는 1개)이면 count, 각자 이름이나 설정이 다른 집합이면 for_each입니다. 실무 코드에서는 for_each 쪽이 훨씬 자주 맞는 답입니다.
dynamic 블록: 중첩 블록의 반복 #
count와 for_each는 리소스 자체를 복제합니다. 그런데 반복이 리소스 안의 중첩 블록에서 필요할 때가 있습니다. 보안 그룹의 ingress 규칙이 대표적입니다.
variable "service_ports" {
default = [80, 443]
}
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.service_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}dynamic "블록이름" 안에 for_each와 content를 적으면, content가 반복 횟수만큼 펼쳐져 ingress 블록이 됩니다. 반복 변수의 이름이 each가 아니라 블록 이름(여기서는 ingress.value)인 점에 주의합니다. 편리하지만 남용하면 코드가 읽기 어려워지므로, 규칙이 두어 개로 고정이라면 그냥 블록을 두 번 적는 편이 명확할 때도 많습니다.
정리 #
이번 글에서 다룬 내용입니다.
- 참조가 곧 의존성입니다. 테라폼은 참조 그래프로 순서를 정하고 의존 없는 리소스는 병렬로 만듭니다
- depends_on은 참조 없는 순서 의존을 위한 최후 수단이며, 남용하면 그래프가 흐려지고 apply가 느려집니다
- count는 번호로 복제합니다.
조건 ? 1 : 0의 조건부 생성 관용구가 유용하지만, 리스트 중간 제거 시 번호가 밀려 의도하지 않은 재생성이 일어납니다 - for_each는 키로 복제하므로 중간 제거에 안전합니다. 이름 있는 집합이면 for_each, 동일 복제와 조건부 생성이면 count입니다
- 중첩 블록의 반복은 dynamic 블록으로 풀고, 반복 변수 이름이 블록 이름과 같다는 점에 주의합니다
다음 글(#7 리소스 수명주기 제어)에서는 #2에서 만난 교체(replace)를 제어하는 방법을 다룹니다. 무중단 교체를 만드는 create_before_destroy, 운영 리소스를 지키는 prevent_destroy, 드리프트를 견디는 ignore_changes입니다.