Skip to content

필사 모드: OpenTofu 1.12의 동적 prevent_destroy — lifecycle의 정적 제약이 풀린 자리와 그 대가

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — "Variables may not be used here"

Terraform을 오래 쓴 사람이라면 이 에러를 만난 적이 있을 겁니다. 데이터베이스 인스턴스를 실수로 날리지 않게 보호하고 싶어서 이렇게 쓰면,

variable "allow_destroy_database" {
  type    = bool
  default = false
}

resource "aws_db_instance" "example" {
  # ...
  lifecycle {
    prevent_destroy = !var.allow_destroy_database
  }
}

Terraform은 거부합니다. lifecycle 블록 안에서는 변수를 쓸 수 없습니다. 운영 환경은 보호하고 개발 환경은 마음껏 지우고 싶어도, 같은 모듈을 두 벌로 복사하거나 count 트릭을 쓰는 것 말고는 방법이 없었습니다.

이게 게으름 때문에 생긴 제약은 아닙니다. HashiCorp의 lifecycle 문서는 이유를 분명히 밝힙니다 — lifecycle 설정은 Terraform이 의존성 그래프를 만들고 순회하는 방식에 영향을 주고, 그래서 "only literal values can be used"라는 것입니다. 임의의 표현식을 평가하기에는 처리 시점이 너무 이르다는 뜻입니다. 그래프를 만들려면 lifecycle 값을 알아야 하는데, 표현식을 평가하려면 그래프가 있어야 합니다. 닭과 달걀입니다.

요청 자체는 오래됐습니다. hashicorp/terraform#10730("Unable to use var for ignore_changes or prevent_destroy")은 2016년 12월 14일에 열려 2019년 8월 27일에 닫혔고, 지금도 열려 있는 정식 요청 #25534("Allow variables in lifecycle block")는 2020년 7월 9일에 열렸습니다. 이 글을 쓰는 시점에 #25534에는 👍 반응이 66개 달려 있습니다. 반응 수는 수요의 정확한 척도가 아니라 약한 신호일 뿐이고, GitHub 반응 수는 언제든 변하는 값이라는 점은 감안하고 읽으십시오.

OpenTofu 1.12가 한 일 — 평가 시점을 옮기다

OpenTofu v1.12.0은 2026년 5월 14일에 릴리스됐고(GitHub 릴리스 메타데이터 기준), 이 제약을 풀었습니다. 공식 문서의 표현으로는, lifecycle 블록의 prevent_destroy 인자가 이제 같은 모듈 안의 다른 심볼 — 예컨대 모듈의 입력 변수 — 을 참조할 수 있습니다.

흥미로운 건 이걸 어떻게 풀었느냐입니다. PR #3474의 설명이 명쾌합니다. 예전에는 설정 로더(config loader) 안에서 prevent_destroy 표현식을 즉시 평가했고, 그래서 불리언을 내놓는 상수 표현식일 수밖에 없었습니다. 이제는 로더가 받은 표현식을 그대로 저장해 두고 평가는 언어 런타임에 맡깁니다. 그러면 같은 모듈 안의 동적으로 결정되는 값을 참조할 수 있게 됩니다.

즉 "그래프를 만들기 전에 알아야 한다"는 전제 자체를 손댄 게 아니라, 평가를 미룰 수 있는 만큼 미룬 것입니다. tofu validate 단계에서는 예전 로더가 하던 것과 동등한 불리언 타입 검사를 여전히 수행하고, nil 평가 컨텍스트로 평가할 수 없는 표현식은 plan 단계로 넘겨 거기서 검사합니다. plan 시점에는 정보가 더 많으니 에러 메시지도 더 정확해진다는 게 PR의 주장입니다.

맨 앞의 예제 — prevent_destroy = !var.allow_destroy_database — 가 이제 OpenTofu에서는 그대로 동작합니다. 원 요청인 opentofu#2522는 2025년 2월 14일에 열렸고 이 PR로 닫혔습니다.

무엇을 참조할 수 있고, 무엇은 안 되나

여기서 정직해질 부분입니다. "동적"이라는 말이 "아무거나 다 된다"는 뜻은 아닙니다.

범위는 같은 모듈 안으로 한정됩니다. 그리고 PR #3474는 까다로운 경우들 — 알 수 없는 값(unknown values), 임시 값(ephemeral values), 민감 값(sensitive values), count.index 같은 로컬 심볼 참조 — 에 대해 의도적으로 가장 보수적인 선택을 했다고 밝힙니다. 이유가 좋습니다. 나중 버전에서 더 허용하는 건 언제든 할 수 있지만, 실수였다는 걸 알게 됐을 때 이미 허용한 걸 도로 막을 수는 없기 때문입니다. 규칙을 푸는 건 하위 호환이지만 조이는 건 아니니까요.

여기서 알 수 있는 실질적 결론은 이겁니다 — 리소스 속성을 참조해 prevent_destroy를 정하는 건 안 됩니다. 리소스 속성은 apply 전까지 unknown일 수 있고, unknown은 보수적으로 막혀 있습니다. 이 기능은 "변수로 환경별 보호 수준을 정하는 것"이지 "런타임 상태를 보고 판단하는 것"이 아닙니다.

이 패턴은 처음이 아닙니다. OpenTofu는 v1.11.0(2025년 12월 9일)에서 이미 enabled 메타 인자lifecycle 블록에 도입했는데, 공식 문서가 밝힌 enabled의 제약이 거의 같은 목록입니다 — unknown 값, 민감 값, null 값, 임시 값, 불리언이 아닌 값은 쓸 수 없고, countfor_each와 함께 쓸 수도 없습니다. lifecycle 블록이 동적이 되는 방식에는 일관된 선이 있는 셈입니다. 값은 동적일 수 있지만, plan 시점에는 알 수 있어야 합니다.

destroy = false — 이건 다른 물건이다

같은 1.12 릴리스에 destroy라는 새 lifecycle 메타 인자가 들어갔습니다. 이름이 비슷해서 묶여 보이지만 성격이 꽤 다르고, 사실 이쪽이 상태 관리에 훨씬 직접적으로 영향을 줍니다.

기본값에서 OpenTofu는 리소스가 설정에서 사라지거나, 교체가 필요하거나, tofu destroy로 명시적으로 지워질 때 실제 인프라 객체를 파괴합니다. destroy = false로 두면 파괴 대신 잊기(forget) — 실제 객체는 그대로 두고 상태(state)에서만 제거 — 를 계획합니다.

resource "aws_db_instance" "example" {
  # ...
  lifecycle {
    destroy = false
  }
}

공식 문서가 드는 용례는 납득이 갑니다. 나머지 환경을 다 내리면서 규제 대상 리소스만 남기고 싶을 때, 교체 중에 옛 인스턴스를 남겨 롤백을 쉽게 하고 싶을 때, 외부 의존성 때문에 애초에 파괴가 불가능한 객체를 다룰 때. 기존에는 tofu state rm을 손으로 때리던 일을, 리뷰 가능한 설정 변경으로 바꿔 준다는 게 요점입니다.

그런데 붙어 있는 조건들이 만만치 않습니다. 하나씩 봅시다.

첫째, 상수만 받습니다. 문서에 못이 박혀 있습니다 — lifecycle.destroy는 상수 불리언 값(true 또는 false)만 받습니다. 방금 prevent_destroy가 동적이 됐다고 했는데, 같은 릴리스에 들어온 destroy는 동적이 아닙니다. lifecycle 블록이 통째로 동적이 된 게 아니라 인자별로 다르다는 뜻이고, 이 비대칭을 모르고 접근하면 헷갈립니다.

둘째, 상태에 눌러앉습니다. 이 인자는 상태에 저장됩니다. 한번 destroy = false를 적용하고 나면, 명시적으로 true로 되돌리거나 설정에서 그 옵션을 지우기 전까지 OpenTofu는 그 리소스의 파괴를 계획하지 않습니다. 마지막으로 적용된 설정을 기준으로 보수적으로 판단하겠다는 것입니다. 단일 인스턴스 리소스(count·for_each 없는)라면 removed 블록에 destroy = true를 써서 상태의 이 값을 덮어쓸 수 있지만, countfor_each를 쓰는 리소스에는 그 탈출구가 없습니다.

셋째, prevent_destroy를 무력화합니다. 이게 제일 중요합니다. 문서의 명시적 경고인데, destroy = false를 설정하면 OpenTofu는 destroy 액션을 forget 액션의 변형으로 바꾸고 prevent_destroy는 아무 효과도 갖지 못합니다. 두 인자를 "안전장치"라는 같은 서랍에 넣고 둘 다 켜 두면 더 안전하겠거니 생각하기 쉬운데, 실제로는 뒤엣것이 앞엣것을 끕니다.

넷째, 잊은 리소스를 되돌리기가 번거롭습니다. 문서의 경고 그대로입니다 — 일단 잊힌(상태에서 제거된) 리소스는 OpenTofu가 더 이상 추적하지 않습니다. 나중에 같은 주소로 설정에 다시 넣으면 OpenTofu는 새 리소스를 만들려고 시도하고, 실제 객체가 아직 살아 있으면 그 시도는 실패할 수 있습니다. 되살리려면 import를 하거나 다른 리소스 주소를 써야 합니다.

다섯째, 종료 코드가 바뀝니다. destroy = false인 리소스가 있는 상태에서 tofu destroy를 돌리면 그것들은 파괴 대신 잊히고, 명령은 일부 리소스가 완전히 제거되지 않았음을 알리려고 0이 아닌 종료 코드로 끝납니다. CI에서 이걸 실패로 읽습니다. 1.12는 이 경우를 위해 tofu destroy -suppress-forget-errors를 같이 추가했습니다.

그리고 하나 더 — 이건 OpenTofu만의 발명이 아닙니다. Terraform에도 removed 블록이 있고, 거기에 destroy = false를 주면 실제 리소스를 파괴하지 않고 상태에서만 제거합니다. 차이는 적용 지점입니다. removed 블록은 설정에서 리소스를 빼면서 쓰는 리팩터링 도구이고, OpenTofu의 lifecycle 인자는 리소스가 설정에 남아 있는 동안 파괴 동작을 바꿉니다. OpenTofu 문서 스스로도 이 선을 긋습니다 — 리팩터링 목적이면 removed 블록을 쓰고, 설정에 남아 있는 리소스의 파괴 동작을 제어할 때 lifecycledestroy를 쓰라고 권합니다.

prevent_destroy가 동적이 돼도 여전히 못 막는 것

동적 prevent_destroy가 반가운 만큼, 이게 고치지 않는 구멍도 분명히 해 둘 필요가 있습니다.

prevent_destroy의 보호는 그 인자가 설정에 남아 있는 동안에만 유효합니다. 문서가 직접 밝히듯, resource 블록을 설정에서 통째로 지우면 prevent_destroy 설정도 같이 사라지고, 그러면 OpenTofu는 파괴를 허용합니다. 즉 이 안전장치는 "이 리소스를 지우는 plan"은 막지만 "이 리소스의 정의를 지우는 커밋"은 막지 못합니다. 실수로 리소스를 날리는 가장 흔한 경로가 바로 후자라는 걸 생각하면, 이건 꽤 큰 한계입니다.

동적으로 만든다고 이게 나아지지 않습니다. 오히려 표면적으로는 나빠질 여지도 있습니다 — 이제 prevent_destroy의 값이 변수에 달려 있으니, tfvars 한 줄이나 CI 환경변수 하나가 보호를 끄는 스위치가 됩니다. 그 스위치가 코드 리뷰를 거치는 곳에 있는지, 아니면 아무나 파이프라인 변수로 덮어쓸 수 있는 곳에 있는지는 이제 여러분의 문제입니다. 기능이 준 건 유연함이고, 유연함은 규율을 요구합니다.

PR #3474가 남긴 열린 질문도 하나 있습니다. 동적 prevent_destroy의 효과를 tofu test로 어떻게 테스트하느냐는 물음인데, PR 저자는 이 PR에서는 전혀 답하지 않았고 관심이 있으면 나중에 따로 다룰 수 있겠다고 적어 뒀습니다. 보호 로직을 변수로 분기시켜 놓고 그 분기를 테스트할 표준적인 방법은 아직 없다는 뜻입니다.

Terraform은 다른 쪽으로 갔다

같은 시기 Terraform은 정적 평가 문제를 전혀 다른 자리에서 풀고 있습니다.

Terraform v1.15.0은 2026년 4월 29일에 나왔고, 새 기능 목록에 이런 항목이 있습니다 — 모듈의 sourceversion 속성에 변수와 로컬을 쓸 수 있게 됐습니다(#38217). 이것도 "설정 로딩 시점에 알아야 하는 값"이라는 같은 계열의 제약을 푼 것입니다. 모듈을 가져오려면 source를 알아야 하는데, 그 값이 변수라면 변수를 먼저 평가해야 하니까요. 방향은 같습니다. 자리가 다를 뿐입니다.

같은 릴리스에는 deprecated 속성을 변수·출력 블록에 붙이는 기능(#38001), 정밀한 인라인 타입 변환을 위한 convert 함수(#38160), backend 블록까지 검사하는 terraform validate(#38021), Windows ARM64 빌드도 들어갔습니다. 그리고 lifecycle 블록은 — 앞서 인용한 문서 문장이 아직 그대로입니다 — 여전히 리터럴만 받습니다.

재미있는 건 수렴도 같이 일어난다는 점입니다. 두 프로젝트 모두 이번 사이클에 s3 백엔드의 aws login 자격 증명 지원을 넣었습니다(OpenTofu #3767, Terraform #37976). 포크가 갈라지기만 하는 게 아니라, 생태계가 같은 요구를 밀어올리면 양쪽이 나란히 같은 걸 만들기도 합니다.

이 글을 쓰는 2026년 7월 16일 기준으로 최신 안정 버전은 OpenTofu가 v1.12.4(7월 13일), Terraform이 v1.15.8(7월 8일)이고, Terraform은 v1.16.0 알파를 굴리는 중입니다.

그래서 이걸 써야 하나

값을 하는 경우

  • 모듈 하나로 dev·staging·prod를 다 커버하면서 보호 수준만 환경별로 다르게 가져가고 싶다. 이게 동적 prevent_destroy의 정확한 사용처이고, 모듈을 복제하거나 count 트릭을 쓰던 것보다 확실히 깔끔합니다.
  • 환경을 내리면서 특정 리소스만 남겨야 하는 절차가 있고, 그걸 지금은 tofu state rm 수작업 룰북으로 굴리고 있다. destroy = false는 그 수작업을 plan에 드러나는 설정 변경으로 바꿔 줍니다.

미루거나 피할 경우

  • 설정이 Terraform과 OpenTofu 양쪽에서 돌아가야 한다. 이 문법을 쓰는 순간 그 설정은 Terraform에서 더 이상 동작하지 않습니다. 포크 사이를 오갈 여지를 남겨 두고 싶다면 이건 편도 티켓입니다. 모듈을 공개 배포한다면 더 그렇습니다 — 사용자 중 누가 어느 쪽을 쓰는지 여러분이 정할 수 없으니까요.
  • 노리는 게 "실수로 리소스 날리는 사고 방지"라면, prevent_destroy는 애초에 그 구멍(설정에서 블록을 지우는 경로)을 못 막습니다. plan 리뷰, -target 금지, CI 게이트, 상태 백업 같은 프로세스 쪽 장치가 여전히 본체이고 이건 보조입니다.
  • destroy = false를 "일단 켜 두면 안전"이라는 생각으로 접근하는 경우. 상태에 눌러앉고, prevent_destroy를 끄고, 되돌리려면 import가 필요합니다. 켜기는 쉽고 끄기는 어려운 스위치입니다.

마치며

정리하면 이렇습니다. Terraform의 lifecycle 블록이 리터럴만 받는 건 의존성 그래프를 만드는 시점에 처리되기 때문이고, 이건 2016년부터 지적돼 온 구조적 제약입니다. OpenTofu 1.12는 prevent_destroy의 평가를 설정 로더에서 언어 런타임으로 옮겨 이 제약을 풀었습니다 — 다만 같은 모듈 안으로 범위를 한정하고, unknown·민감·임시 값과 count.index는 의도적으로 보수적으로 막아 둔 채로. 되돌릴 수 없는 실수를 피하려고 좁게 시작한 것이고, 저는 이 판단이 옳다고 봅니다.

같은 릴리스의 destroy = false는 이름값보다 훨씬 무거운 물건입니다. 상수만 받고, 상태에 저장되고, prevent_destroy를 무력화하고, 잊은 리소스를 되돌리려면 import가 필요하고, tofu destroy의 종료 코드를 바꿉니다. 유용하지만 켜기 전에 이 다섯 개를 다 알고 켜야 합니다.

그리고 큰 그림에서 보면, 이건 포크가 "누가 더 낫냐"로 갈리는 이야기가 아닙니다. 양쪽 다 정적 평가라는 같은 벽을 밀고 있고, 미는 자리가 다를 뿐입니다 — OpenTofu는 lifecycle을, Terraform은 모듈 sourceversion을. 그 결과로 실무자에게 실제로 남는 건 기능 목록이 아니라 이식성 결정입니다. 이 문법을 쓰는 순간 여러분의 설정은 한쪽에만 속하게 됩니다. 그게 괜찮은지가, 기능이 멋진지보다 먼저 답해야 할 질문입니다.

참고 자료

현재 단락 (1/67)

Terraform을 오래 쓴 사람이라면 이 에러를 만난 적이 있을 겁니다. 데이터베이스 인스턴스를 실수로 날리지 않게 보호하고 싶어서 이렇게 쓰면,

작성 글자: 0원문 글자: 8,356작성 단락: 0/67