Terraform lifecycle

들어가며: “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이 변경되면:

  1. 기존 리소스 삭제 (서비스 중단 발생)
  2. 새 리소스 생성

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
  }
}

이제 교체 순서가 생성 → 삭제로 바뀐다:

  1. 새 리소스 먼저 생성
  2. 의존하는 리소스 업데이트
  3. 기존 리소스 삭제

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. 제거가 필요할 때

리소스를 정말 삭제해야 할 때는:

  1. 코드에서 prevent_destroy = false로 변경
  2. terraform apply로 설정 반영
  3. 리소스 블록 제거 또는 terraform destroy

주의: prevent_destroyterraform 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. 실전 체크리스트

  1. 운영 DB/스토리지: prevent_destroy = true가 설정되었는가?
  2. 교체 필요 리소스: 고정 name 대신 name_prefix를 사용하여 create_before_destroy가 동작하는가?
  3. Auto Scaling 속성: desired_count/desired_capacity/replicasignore_changes에 추가했는가?
  4. ignore_changes 주석: 왜 무시하는지 주석으로 남겼는가? 분기별 리뷰 일정이 있는가?
  5. replace_triggered_by: in-place update로는 반영되지 않는 변경(User Data 등)에 교체 트리거를 설정했는가?
  6. 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 한 번에 서비스가 죽는” 사고를 예방할 수 있다.

참고 자료

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux