LabHub
배우기 러닝패스 코스

Terraform/OpenTofu Fundamentals

How to Read a Plan — Symbols, Exit Codes, Drift

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

applyplan 이 이미 내린 결정을 실행할 뿐이다. 사고를 막을 수 있는 유일한 자리는 계획을 읽는 몇 분이고, 그 몇 분은 결국 사람이 아니라 스크립트가 대신해야 한다.

Concept map: 파일로 저장해 · 기계가 읽는 형식으로 뽑아 규칙으로 판정 · 변경할 수 없는 인자 · 조회 단계에서 발견한 코드 밖 변경

왜 이게 필요했나

인프라 코드에서 가장 비싼 실수는 "재생성을 제자리 수정으로 잘못 읽은 것"이다. 화면에 뜬 -/+~ 로 착각하면 프로덕션 데이터베이스가 지워졌다가 다시 만들어진다. 계획 출력은 길고, 배포는 대개 늦은 시간에 이뤄지며, 사람의 눈은 같은 화면을 세 번 보면 읽지 않기 시작한다.

그래서 성숙한 팀은 두 가지를 한다. 첫째, 계획을 파일로 저장해 리뷰한 그 계획을 그대로 적용한다. 저장하지 않으면 리뷰한 계획과 실제 적용되는 계획이 다를 수 있다. 그 사이에 누군가 다른 것을 바꿨을 수 있기 때문이다. 둘째, 계획을 기계가 읽는 형식으로 뽑아 규칙으로 판정한다. "삭제가 하나라도 있으면 사람 승인" 같은 규칙은 사람이 아니라 파이프라인이 지켜야 한다.

어떻게 동작하나

사람이 읽는 계획에는 기호가 붙는다.

기호 위험도
+ 생성 낮음
~ 제자리 수정 보통
- 삭제 높음
-/+ 삭제 후 재생성 매우 높음
+/- 생성 후 삭제 (create_before_destroy) 높음
<= 데이터 소스 읽기 없음

재생성이 나오는 이유는 대개 변경할 수 없는 인자를 건드렸기 때문이고, 그런 줄에는 # forces replacement 표시가 붙는다. 맨 아래 요약 줄인 Plan: X to add, Y to change, Z to destroy 에서 재생성은 add 와 destroy 양쪽에 1씩 잡힌다는 점도 기억해야 한다.

자동화에 필요한 것은 기호가 아니라 두 가지다. 하나는 -detailed-exitcode 다.

종료 코드 의미
0 변경 없음
1 오류
2 변경 있음

이 세 값 덕분에 "변경이 있으면 알린다" 같은 정기 드리프트 감지를 셸 한 줄로 만들 수 있다. 다른 하나는 JSON 표현이다. 계획 파일을 JSON 으로 바꾸면 resource_changes 배열이 나오고, 각 항목의 change.actions["create"], ["update"], ["delete"], 재생성이면 ["delete","create"] 로 들어 있다. 그리고 resource_drift 에는 조회 단계에서 발견한 코드 밖 변경이 따로 담긴다. 계획에 반영될 변경과 이미 벌어진 변경을 구분해서 볼 수 있다는 뜻이다.

-target 은 그래프의 일부만 골라 처리하는 옵션이다. 쓰면 도구가 경고를 낸다. 일부만 적용하면 나머지가 코드와 어긋난 채로 남아 상태가 일관되지 않을 수 있기 때문이다. 복구 상황의 도구이지 평소 워크플로가 아니다.

현장에서 만나는 모습

첫째, 프로바이더 업그레이드 후의 대량 드리프트. 메이저 버전을 올리면 새로 추가된 속성의 기본값 때문에 수백 개 리소스에 변경이 뜬다. 대부분은 실제 인프라를 바꾸는 것이 아니라 상태에 속성을 채워 넣는 것이다. 이 둘을 구분하지 않고 놀라서 되돌리거나, 반대로 확인 없이 적용하는 것이 모두 사고로 이어진다. JSON 으로 뽑아 동작별로 세어 보면 판단이 빨라진다.

둘째, 감지 자체의 비용. plan 은 관리 중인 모든 리소스에 대해 프로바이더 API 를 부른다. 규모가 크면 API 제한에 걸리고, 감지 도중에는 상태 잠금이 걸려 배포와 충돌한다. 감지 주기는 공짜가 아니다.

셋째, 계획 로그에 남는 비밀. 계획 출력에는 리소스 속성이 그대로 찍힌다. 알림 채널이나 CI 로그에 통째로 붙여 넣는 습관은 그대로 유출 경로가 된다. 요약만 보내고 전체는 접근이 통제된 곳에 두는 것이 안전하다.

계획에서 반드시 눈으로 확인할 세 가지

terraform plan 의 출력은 길다. 다 읽을 수 없으므로 무엇을 찾을지 정해 두고 본다.

첫째, 요약 줄의 destroy 개수. Plan: 3 to add, 1 to change, 2 to destroy 에서 0이 아닌 destroy 는 언제나 손을 멈추고 볼 이유다. 의도한 것이라면 무엇이 지워지는지 이름으로 확인하고, 의도하지 않았다면 대개 자원 주소가 바뀐 것이다(모듈을 옮겼거나 countfor_each 로 바꿨거나). 그때는 지웠다 만드는 대신 moved 블록으로 주소만 옮긴다.

moved {
  from = aws_instance.web[0]
  to   = aws_instance.web["a"]
}

둘째, # forces replacement 가 붙은 속성. 이름 하나 바꿨을 뿐인데 데이터베이스가 교체되는 계획이 여기서 드러난다. 상태 있는 자원에는 prevent_destroy 를 걸어 두면 계획 단계에서 아예 막힌다.

lifecycle { prevent_destroy = true }

셋째, (known after apply) 가 어디에 붙었는가. 이 값이 많으면 계획이 실제로 무엇을 할지 미리 알기 어렵다는 뜻이다. 다른 자원의 아직 없는 속성을 참조하기 때문인데, 그 자체는 정상이지만 중요한 결정(보안 그룹 규칙, 정책 문서)이 이 값에 달려 있다면 적용 전에는 검토할 수 없다는 것을 알고 있어야 한다.

계획을 파일로 받아 적용한다. 계획과 적용 사이에 누군가 무언가를 바꾸면, 본 것과 다른 것이 적용된다.

terraform plan -out=tfplan
terraform show -json tfplan | jq '[.resource_changes[]
  | select(.change.actions | index("delete"))] | map(.address)'
terraform apply tfplan

자동화에서는 종료 코드를 쓴다. -detailed-exitcode 는 0(변경 없음), 1(오류), 2(변경 있음)를 돌려준다. CI 에서 "변경이 있으면 사람의 승인을 받는" 흐름이 여기서 나온다.

다음 실습에서 할 것

/root/tf/plan 에서 계획을 파일로 저장해 JSON 으로 바꾸고, 동작별 개수를 세어 기록한다. -detailed-exitcode 의 두 가지 종료 코드를 직접 확인하고, 제자리 수정과 재생성이 한 계획 안에 함께 잡히게 만든다. 관리 중인 파일을 손으로 고쳐 resource_drift 를 보고, -target 으로 범위를 좁히며 도구의 경고를 읽는다. 마지막에는 삭제가 포함된 계획을 스스로 판정하는 리뷰 보고서를 만든다.