들어가며: “terraform apply 했더니 리소스가 삭제됐다가 다시 생성됐어요”
Terraform을 운영에서 쓰다 보면, 인프라 변경이 예상과 다르게 동작하는 순간이 온다. 보안 그룹 교체 시 기존 리소스가 먼저 삭제되어 서비스가 끊기거나, 누군가 콘솔에서 변경한 태그 때문에 매번 diff가 발생하거나, 실수로 운영 DB를 삭제하거나. 이 모든 문제를 코드 레벨에서 방어하는 것이 lifecycle 블록이다.
이 글에서는 Terraform 공식 문서(The lifecycle Meta-Argument)를 근거로, create_before_destroy, prevent_destroy, ignore_changes, replace_triggered_by 네 가지 규칙의 동작 원리와 실무 패턴을 다룬다.
1. lifecycle 블록 개요
1-1. 기본 문법
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [tags]
replace_triggered_by = [null_resource.trigger.id]
}
}
Terraform 공식 문서에 따르면, lifecycle은 모든 resource 블록에서 사용할 수 있는 메타 인수(meta-argument)다. data 소스나 module에는 직접 사용할 수 없다.
1-2. 4가지 규칙 요약
| 규칙 | 기본값 | 효과 |
|---|---|---|
create_before_destroy |
false |
교체 시 새 리소스를 먼저 생성한 뒤 기존 리소스 삭제 |
prevent_destroy |
false |
리소스 삭제를 차단 (plan 단계에서 에러) |
ignore_changes |
[] |
지정한 속성의 외부 변경을 무시 |
replace_triggered_by |
[] |
지정한 리소스/속성 변경 시 강제 교체 |
2. create_before_destroy: 무중단 교체의 핵심
2-1. 기본 동작 (destroy-then-create)
Terraform의 기본 교체 순서는 삭제 → 생성이다. 예를 들어 Launch Template이 변경되면:
- 기존 리소스 삭제 (서비스 중단 발생)
- 새 리소스 생성
2-2. create_before_destroy 적용
resource "aws_launch_template" "web" {
name_prefix = "web-"
image_id = var.ami_id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
}
}
이제 교체 순서가 생성 → 삭제로 바뀐다:
- 새 리소스 먼저 생성
- 의존하는 리소스 업데이트
- 기존 리소스 삭제
2-3. 주의: name 충돌
리소스에 고정된 name이 있으면 create_before_destroy가 실패한다. 같은 이름의 리소스가 동시에 존재할 수 없기 때문이다.
# ❌ name이 고정이면 충돌
resource "aws_security_group" "web" {
name = "web-sg" # 동일 이름으로 생성 시도 → 에러
lifecycle {
create_before_destroy = true
}
}
# ✅ name_prefix 사용으로 해결
resource "aws_security_group" "web" {
name_prefix = "web-sg-" # web-sg-abc123 같은 유니크 이름 생성
lifecycle {
create_before_destroy = true
}
}
Terraform 공식 문서에서 이 제약을 명시하고 있으며, name_prefix를 사용하거나 이름에 랜덤 접미사를 추가하는 것을 권장한다.
2-4. 실무 적용 대상
| 리소스 | 적용 이유 |
|---|---|
| Launch Template / Launch Configuration | ASG가 참조하는 동안 삭제하면 인스턴스 교체 불가 |
| SSL 인증서 (ACM) | 새 인증서 발급 후 구 인증서 삭제해야 HTTPS 끊김 방지 |
| Security Group | ENI가 참조 중이면 삭제 불가 → 교착 상태 방지 |
| IAM Role / Policy | 서비스가 사용 중인 역할을 먼저 삭제하면 권한 에러 |
3. prevent_destroy: 운영 리소스 삭제 방어
3-1. 동작
resource "aws_db_instance" "production" {
identifier = "prod-mysql"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.r6g.large"
lifecycle {
prevent_destroy = true
}
}
prevent_destroy = true가 설정된 리소스를 삭제하려고 하면, terraform plan 단계에서 에러가 발생한다:
Error: Instance cannot be destroyed
on main.tf line 1:
1: resource "aws_db_instance" "production" {
Resource aws_db_instance.production has lifecycle.prevent_destroy set,
but the plan calls for this resource to be destroyed.
3-2. 적용 대상
- RDS 인스턴스 (운영 DB)
- S3 버킷 (데이터 저장소)
- KMS 키 (암호화 키)
- EIP (고정 IP, DNS 연결)
3-3. 제거가 필요할 때
리소스를 정말 삭제해야 할 때는:
- 코드에서
prevent_destroy = false로 변경 terraform apply로 설정 반영- 리소스 블록 제거 또는
terraform destroy
주의: prevent_destroy는 terraform destroy 전체 삭제도 차단한다. 의도적으로 설계된 동작이다(Terraform 공식 문서).
4. ignore_changes: 외부 변경을 무시
4-1. 문제 상황
Terraform으로 관리하는 리소스를 콘솔이나 다른 도구에서 변경하면, 다음 terraform plan에서 diff가 발생한다. 매번 되돌리는 것이 올바른 경우도 있지만, 의도적으로 무시해야 하는 경우도 있다.
4-2. 특정 속성 무시
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
lifecycle {
ignore_changes = [
tags, # 콘솔에서 추가한 태그 무시
ami, # AMI 업데이트는 별도 프로세스로 관리
]
}
}
4-3. 전체 속성 무시 (all)
resource "aws_instance" "legacy" {
ami = "ami-legacy"
instance_type = "t2.micro"
lifecycle {
ignore_changes = all # 모든 속성 변경 무시
}
}
ignore_changes = all은 Terraform이 리소스의 생성/삭제만 관리하고, 속성 변경은 완전히 무시한다. import한 레거시 리소스를 “읽기 전용”으로 관리할 때 유용하다.
4-4. 실무 사용 사례
| 속성 | 무시 이유 |
|---|---|
ECS desired_count |
Auto Scaling이 동적으로 변경하므로 Terraform이 되돌리면 안 됨 |
ASG desired_capacity |
Auto Scaling 정책에 의해 변경됨 |
tags |
AWS Config나 조직 정책이 자동으로 태그 추가 |
K8s Deployment replicas |
HPA가 동적으로 변경 |
Lambda last_modified |
배포 시마다 변경되는 읽기 전용 속성 |
4-5. 주의: ignore_changes 남용 금지
ignore_changes에 속성을 추가할 때마다 Terraform이 관리하지 않는 영역이 넓어진다. 이는 “코드 = 인프라”라는 IaC 원칙을 약화시킨다. 반드시 “왜 무시하는가”를 주석으로 남기고, 정기적으로 목록을 리뷰해야 한다.
5. replace_triggered_by: 연쇄 교체 트리거
5-1. Terraform 1.2+ 신규 기능
다른 리소스의 변경이 현재 리소스의 교체를 강제해야 할 때 사용한다. -replace 플래그의 선언적 버전이다.
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
replace_triggered_by = [
aws_security_group.web.id, # SG 변경 시 인스턴스 교체
null_resource.deploy_trigger.id,
]
}
}
resource "null_resource" "deploy_trigger" {
triggers = {
deploy_version = var.deploy_version # 배포 버전 변경 시 트리거
}
}
5-2. 사용 사례
| 트리거 | 대상 | 이유 |
|---|---|---|
| Security Group 변경 | EC2 인스턴스 | SG 교체 시 인스턴스도 재생성해야 새 SG 적용 |
| User Data 변경 | EC2 인스턴스 | User Data는 인스턴스 생성 시에만 실행됨 |
| null_resource trigger | 임의 리소스 | 수동 트리거가 필요한 경우 |
6. 조합 패턴: 실전에서 자주 쓰는 lifecycle 세트
6-1. 운영 DB 보호 세트
resource "aws_db_instance" "production" {
identifier = "prod-mysql"
engine = "mysql"
instance_class = "db.r6g.large"
lifecycle {
prevent_destroy = true
ignore_changes = [
engine_version, # 엔진 업그레이드는 콘솔/RDS 콘솔에서 관리
]
}
}
6-2. 무중단 배포 세트
resource "aws_launch_template" "app" {
name_prefix = "app-"
image_id = var.ami_id
lifecycle {
create_before_destroy = true
}
}
resource "aws_autoscaling_group" "app" {
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
lifecycle {
ignore_changes = [desired_capacity] # ASG가 관리
}
}
6-3. 레거시 import + 읽기 전용
import {
to = aws_vpc.legacy
id = "vpc-abc123"
}
resource "aws_vpc" "legacy" {
cidr_block = "10.0.0.0/16"
lifecycle {
prevent_destroy = true
ignore_changes = all
}
}
7. 실전 체크리스트
- 운영 DB/스토리지:
prevent_destroy = true가 설정되었는가? - 교체 필요 리소스: 고정
name대신name_prefix를 사용하여create_before_destroy가 동작하는가? - Auto Scaling 속성:
desired_count/desired_capacity/replicas를ignore_changes에 추가했는가? - ignore_changes 주석: 왜 무시하는지 주석으로 남겼는가? 분기별 리뷰 일정이 있는가?
- replace_triggered_by: in-place update로는 반영되지 않는 변경(User Data 등)에 교체 트리거를 설정했는가?
- plan 검증:
terraform plan에서 예상치 못한 destroy/recreate가 나오면 lifecycle 규칙으로 방어했는가?
8. 흔한 실수와 방지법
| 실수 | 증상 | 방지법 |
|---|---|---|
create_before_destroy + 고정 name |
이름 충돌로 생성 실패 | name_prefix 사용 또는 이름에 랜덤 접미사 추가 |
prevent_destroy 해제 없이 리소스 블록 제거 |
terraform plan 에러 |
먼저 prevent_destroy = false로 변경 → apply → 블록 제거 |
ignore_changes 남용 |
코드와 실제 인프라 괴리 → drift 확대 | 무시 사유를 주석으로 남기고, 분기별 리뷰 |
replace_triggered_by에 자주 변경되는 속성 지정 |
불필요한 리소스 재생성 반복 | 트리거를 안정적인 속성(id, arn)으로 지정 |
정리
Terraform lifecycle은 “인프라 변경이 어떤 순서로, 어떤 조건에서 일어나는가”를 코드로 제어하는 메타 인수다. create_before_destroy로 무중단 교체, prevent_destroy로 삭제 방어, ignore_changes로 외부 변경 공존, replace_triggered_by로 연쇄 교체를 설계한다. 운영 환경에서는 이 네 가지를 의식적으로 설계해야 “apply 한 번에 서비스가 죽는” 사고를 예방할 수 있다.