테라폼 - 순환 참조는 왜 발생하고 어떻게 끊어야 할까?
Terraform 의존성 그래프와 Phase 분리로 이해하는 순환 참조 해결
이전 글에서는 terraform apply가 성공했는데도 서비스가 정상적으로 동작하지 않는 이유를 정리했다.
핵심은 다음 흐름이었다.
Create → Ready → Attach → Activate
Terraform은 리소스를 생성하고 state에 기록하는 데 강하다. 하지만 실제 서비스는 생성 이후에도 준비, 연결, 실행 단계를 거쳐야 완성된다.
이번 글에서는 이 개념을 바탕으로 Terraform에서 자주 만나는 또 다른 문제를 다뤄보려 한다.
순환 참조(Circular Dependency)
순환 참조는 Terraform을 처음 사용할 때는 잘 드러나지 않지만, 인프라 구조가 조금만 복잡해져도 쉽게 마주칠 수 있는 문제다.
목차
- 1. 순환 참조란 무엇인가
- 2. Terraform은 왜 순환 참조를 싫어할까?
- 3. 단순 의존성과 순환 의존성의 차이
- 4. 실제 인프라에서 순환 참조가 발생하는 이유
- 5. depends_on으로 해결할 수 있을까?
- 6. 순환 참조는 구조가 아니라 시간으로 끊는다
- 7. 예시 1: ECS와 ALB
- 8. 예시 2: CloudFront, ACM, DNS
- 9. 예시 3: Lambda와 EventBridge
- 10. 순환 참조를 줄이는 설계 원칙
- 11. 마무리
1. 순환 참조란 무엇인가
순환 참조는 간단히 말하면 다음과 같은 구조다.
A를 만들기 위해 B가 필요하다.
B를 만들기 위해 A가 필요하다.
그림처럼 표현하면 다음과 같다.
A → B
B → A
이 구조에서는 무엇을 먼저 만들어야 할지 결정할 수 없다.
A를 먼저 만들려면 B가 필요하고, B를 먼저 만들려면 A가 필요하기 때문이다.
Terraform에서는 이런 구조가 발생하면 보통 다음과 같은 형태의 오류를 보게 된다.
Error: Cycle: ...
즉, Terraform이 리소스 생성 순서를 계산하는 과정에서 순환 구조를 발견했다는 의미다.
2. Terraform은 왜 순환 참조를 싫어할까?
Terraform은 리소스 간 참조 관계를 분석해서 내부적으로 의존성 그래프를 만든다.
예를 들어 다음 코드가 있다고 하자.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
이 코드는 Subnet이 VPC를 참조하고 있다.
Terraform은 이를 보고 다음 순서를 만든다.
aws_vpc.main
→ aws_subnet.public
이런 구조는 문제가 없다. 의존 방향이 한쪽으로만 흐르기 때문이다.
Terraform이 원하는 의존성 그래프는 기본적으로 다음과 같은 구조다.
방향은 있음
하지만 순환은 없음
이런 구조를 DAG라고 부른다.
DAG = Directed Acyclic Graph
방향이 있는 비순환 그래프
Terraform은 이 그래프를 기반으로 리소스 생성 순서를 결정한다.
따라서 순환이 생기면 Terraform은 어떤 리소스를 먼저 만들어야 하는지 결정할 수 없다.
3. 단순 의존성과 순환 의존성의 차이
단순 의존성은 한 방향으로만 흐른다.
VPC → Subnet → EC2
이 경우 순서가 명확하다.
1. VPC 생성
2. Subnet 생성
3. EC2 생성
하지만 순환 의존성은 방향이 다시 되돌아온다.
A → B → C → A
이 경우 시작점을 결정할 수 없다.
중요한 점은 다음이다.
단순 의존성은 Terraform이 자동으로 처리할 수 있지만, 순환 의존성은 Terraform이 자동으로 해결할 수 없다.
4. 실제 인프라에서 순환 참조가 발생하는 이유
이론적으로는 순환 참조가 이상해 보일 수 있다. 하지만 실제 인프라에서는 꽤 자연스럽게 발생한다.
왜냐하면 인프라는 단순히 리소스를 나열하는 구조가 아니라, 서로 연결된 시스템이기 때문이다.
4.1 ECS와 ALB
ECS Service를 외부에 노출하려면 보통 ALB와 Target Group이 필요하다.
ECS Service
→ Target Group 필요
→ ALB Listener 필요
반대로 ALB의 Health Check는 ECS Task가 정상적으로 응답해야 통과한다.
Target Group Health Check
→ ECS Task 응답 필요
즉, 구조적으로는 서로 연결되어 있다.
ECS Service ↔ Target Group / ALB
물론 이 관계가 항상 Terraform cycle 오류로 이어지는 것은 아니다. 하지만 설계를 잘못하면 생성, 연결, 실행이 한 번에 섞이면서 순환 구조나 배포 실패로 이어질 수 있다.
4.2 CloudFront, ACM, DNS
CloudFront에 커스텀 도메인을 연결하려면 ACM 인증서가 필요하다.
CloudFront
→ ACM 인증서 필요
ACM 인증서를 DNS 검증 방식으로 발급하려면 DNS 레코드가 필요하다.
ACM
→ DNS 검증 레코드 필요
그리고 실제 도메인 트래픽은 Route53 Record를 통해 CloudFront로 연결된다.
Route53
→ CloudFront 도메인 필요
이 구조를 한 번에 묶어서 생각하면 다음처럼 보일 수 있다.
CloudFront → ACM → DNS → CloudFront
이런 경우 어떤 부분을 먼저 만들고, 어떤 부분을 나중에 연결할지 분리하지 않으면 구조가 꼬이기 쉽다.
4.3 Lambda와 EventBridge
EventBridge가 Lambda를 호출하려면 Lambda 함수가 필요하다.
EventBridge Rule
→ Lambda ARN 필요
반대로 Lambda가 EventBridge에서 호출되려면 Lambda Permission이 필요하다.
Lambda Permission
→ EventBridge Rule ARN 필요
이 구조도 서로를 참조하는 것처럼 보일 수 있다.
Lambda ↔ EventBridge
이럴 때는 Lambda, EventBridge Rule, Lambda Permission을 명확히 분리해서 생각해야 한다.
5. depends_on으로 해결할 수 있을까?
Terraform에는 depends_on이라는 기능이 있다.
명시적으로 리소스 간 순서를 지정할 때 사용한다.
resource "aws_instance" "app" {
ami = "ami-xxxx"
instance_type = "t3.micro"
depends_on = [
aws_security_group.app
]
}
하지만 depends_on은 만능이 아니다.
depends_on은 다음 상황에서는 유용하다.
Terraform이 자동으로 의존성을 추론하지 못하지만,
실제로는 순서가 필요한 경우
예를 들어 어떤 리소스가 직접 값을 참조하지는 않지만, 실제로는 먼저 생성되어야 하는 경우가 있다.
하지만 순환 참조는 다르다.
A depends_on B
B depends_on A
이런 구조에서는 depends_on을 추가해도 해결되지 않는다. 오히려 순환을 더 명확하게 만들 뿐이다.
depends_on은 순서를 보완하는 도구이지, 순환 참조를 해결하는 도구가 아니다.
6. 순환 참조는 구조가 아니라 시간으로 끊는다
순환 참조를 해결하는 핵심은 리소스를 더 억지로 연결하는 것이 아니다.
중요한 것은 리소스의 생명주기를 단계로 나누는 것이다.
Create → Ready → Attach → Activate
즉, 동시에 서로를 필요로 하는 것처럼 보이는 구조를 시간 순서로 나누어 처리한다.
순환 구조는 다음과 같이 바꿀 수 있다.
Before:
A ↔ B
After:
1. A 생성
2. B 생성
3. A와 B 연결
4. 실행
여기서 중요한 것은 이것이다.
리소스를 나누는 것이 아니라, 리소스의 생명주기를 나눈다.
Terraform 코드 안에서 모든 것을 한 번에 처리하려 하면 순환이 생기거나 배포가 불안정해질 수 있다.
반대로 생성, 준비, 연결, 실행을 분리하면 순환처럼 보이던 구조도 순서가 생긴다.
7. 예시 1: ECS와 ALB
ECS와 ALB는 가장 흔한 예시다.
초보자 입장에서는 다음처럼 생각하기 쉽다.
ECS Service를 만들면서
ALB Target Group에 바로 연결하고
desired_count도 바로 올린다.
하지만 이렇게 하면 다음 문제가 한 번에 섞인다.
Service 생성
Target Group 연결
Task 실행
Health Check
트래픽 수신
이 문제를 Phase로 나누면 더 안정적이다.
7.1 Phase 분리
1. ALB 생성
2. Target Group 생성
3. Listener 생성
4. ECS Cluster 생성
5. Task Definition 생성
6. ECS Service 생성 (desired_count = 0)
7. ECS Service와 Target Group 연결
8. desired_count 증가
9. Health Check 확인
핵심은 ECS Service를 처음부터 실행 상태로 두지 않는 것이다.
resource "aws_ecs_service" "app" {
name = "app-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 0
}
그리고 이후 CI/CD에서 scale up 한다.
aws ecs update-service \
--cluster app-cluster \
--service app-service \
--desired-count 2
이렇게 하면 Terraform은 구조를 만들고, CI/CD는 실행 시점을 제어할 수 있다.
7.2 왜 이게 중요한가
ECS Task가 실행되기 시작하면 실제 런타임 문제가 드러난다.
이미지 pull 실패
환경 변수 오류
Secret 참조 실패
DB 연결 실패
Health Check 실패
따라서 처음부터 Terraform apply 단계에서 실행까지 모두 처리하기보다, 실행은 배포 파이프라인에서 다루는 것이 더 안전할 수 있다.
8. 예시 2: CloudFront, ACM, DNS
CloudFront와 ACM, DNS는 순서가 특히 중요하다.
커스텀 도메인을 사용하는 CloudFront를 만들려면 ACM 인증서가 필요하다. 그리고 CloudFront용 ACM 인증서는 일반적으로 us-east-1 리전에 있어야 한다.
흐름을 잘못 잡으면 인증서 검증, CloudFront 생성, Route53 연결이 꼬일 수 있다.
8.1 안전한 흐름
1. ACM 인증서 요청
2. DNS validation record 생성
3. ACM validation 완료 대기
4. CloudFront Distribution 생성
5. CloudFront Deployed 대기
6. Route53 Alias Record 생성
7. 트래픽 확인
이 흐름에서 중요한 점은 ACM 검증이 완료되기 전에는 CloudFront에 인증서를 안정적으로 연결하기 어렵다는 것이다.
또한 CloudFront Distribution은 생성 후에도 배포 완료까지 시간이 걸린다.
CloudFront Status:
InProgress → Deployed
따라서 CloudFront를 생성했다고 해서 바로 Route53 연결이나 트래픽 검증을 진행하는 것은 위험할 수 있다.
8.2 Terraform 코드에서 주의할 점
ACM DNS 검증을 Route53으로 자동화할 수 있다면 다음 흐름을 만들 수 있다.
aws_acm_certificate
→ aws_route53_record (validation)
→ aws_acm_certificate_validation
→ aws_cloudfront_distribution
→ aws_route53_record (alias)
하지만 외부 DNS를 사용한다면 검증 레코드를 수동으로 등록해야 할 수 있다. 이 경우 Terraform apply만으로 전체 자동화가 끝나지 않는다.
이런 리소스는 Manual-step 또는 External-dependency 성격을 가진다.
9. 예시 3: Lambda와 EventBridge
Lambda와 EventBridge는 연결 리소스를 분리해서 보면 이해하기 쉽다.
필요한 리소스는 보통 다음과 같다.
1. Lambda Function
2. EventBridge Rule
3. Event Target
4. Lambda Permission
각각의 역할은 다르다.
Lambda Function
→ 실행 대상
EventBridge Rule
→ 실행 조건 또는 스케줄
Event Target
→ Rule이 호출할 대상
Lambda Permission
→ EventBridge가 Lambda를 호출할 수 있는 권한
이 구조를 한 덩어리로 보면 순환처럼 느껴질 수 있다. 하지만 실제로는 역할을 나누면 순서가 생긴다.
9.1 안전한 흐름
1. Lambda Function 생성
2. EventBridge Rule 생성
3. Event Target 생성
4. Lambda Permission 생성
5. Rule enable
특히 Rule을 바로 활성화하면 준비되지 않은 Lambda를 호출할 수 있다. 따라서 초기에는 비활성 상태로 두고, 연결이 끝난 뒤 활성화하는 방식도 고려할 수 있다.
resource "aws_cloudwatch_event_rule" "schedule" {
name = "worker-schedule"
schedule_expression = "rate(1 minute)"
is_enabled = false
}
이후 CI/CD나 운영 단계에서 활성화할 수 있다.
aws events enable-rule \
--name worker-schedule
10. 순환 참조를 줄이는 설계 원칙
순환 참조를 완전히 없애는 것은 쉽지 않다. 하지만 줄일 수는 있다.
다음 원칙을 지키면 Terraform 코드가 훨씬 안정적이 된다.
10.1 생성과 연결을 분리한다
리소스 생성과 리소스 연결을 한 번에 처리하지 않는다.
리소스 생성
→ 준비 상태 확인
→ 연결
→ 실행
이 흐름을 유지하면 순환처럼 보이던 구조도 단계적으로 풀 수 있다.
10.2 실행은 마지막에 한다
ECS desired_count, Scheduler enable, 트래픽 연결 같은 작업은 실행 단계에 가깝다.
이런 작업은 Terraform apply와 분리해서 CI/CD에서 처리하는 것이 더 안전할 수 있다.
Terraform:
- 리소스 생성
- 기본 구조 구성
CI/CD:
- 대기
- 연결 확인
- 실행
- 헬스체크
10.3 시스템을 리소스 단위로 분해한다
ECS나 CloudFront 같은 서비스를 하나의 리소스로 생각하면 구조가 복잡해진다.
항상 Terraform resource 단위로 나누어 봐야 한다.
ECS 전체 X
aws_ecs_cluster
aws_ecs_task_definition
aws_ecs_service
aws_lb_target_group
aws_lb_listener
load_balancer block
이렇게 분해하면 어떤 리소스가 생성 조건이고, 어떤 설정이 연결 조건이며, 어떤 값이 실행 조건인지 구분할 수 있다.
10.4 State도 계층별로 나눌 수 있다
규모가 커지면 모든 리소스를 하나의 state에서 관리하는 것도 문제가 될 수 있다.
예를 들어 다음처럼 state를 나눌 수 있다.
network
platform
data
app
edge
이렇게 나누면 하위 계층의 결과를 상위 계층에서 참조하는 방식으로 의존성 방향을 강제할 수 있다.
network → data → app → edge
State 분리에 대한 자세한 내용은 이후 글에서 따로 정리할 예정이다.
11. 마무리
Terraform에서 순환 참조는 단순한 코드 오류가 아니다.
실제 인프라가 서로 연결된 시스템이기 때문에 자연스럽게 발생할 수 있는 문제다.
중요한 것은 순환 참조를 무조건 depends_on으로 해결하려고 하지 않는 것이다.
순환 참조는 구조로 억지로 풀기보다, 리소스의 생명주기를 시간 순서로 나누어 해결하는 것이 더 안정적이다.
Create → Ready → Attach → Activate
이 흐름을 기준으로 보면 복잡한 인프라도 다음처럼 나누어 이해할 수 있다.
먼저 만든다.
준비될 때까지 기다린다.
연결한다.
마지막에 실행한다.
한 줄 정리
순환 참조는 구조가 아니라 시간으로 끊는다.
다음 글에서는 Terraform State를 왜 나누어야 하는지, 그리고 State 분리가 의존성 관리와 destroy 안정성에 어떤 영향을 주는지 정리해본다.
'테라폼' 카테고리의 다른 글
| 3-4. 테라폼 - CI/CD에서 Terraform은 어디까지 맡아야 할까? (1) | 2026.05.05 |
|---|---|
| 3-3. 테라폼 - State는 왜 나눠야 할까? (2) | 2026.05.05 |
| 3-1. 테라폼 - apply는 성공했는데 왜 서비스는 동작하지 않을까? (0) | 2026.05.05 |
| 3-0. 테라폼 - 리소스의 분류 및 구현 (0) | 2026.05.05 |
| 2. 테라폼 - Terraform 프로젝트 구조 (0) | 2026.04.28 |