terraform plan은 성공했는데 apply가 실패할 때: 원인과 부분 적용 복구

5 분 소요

terraform plan은 깨끗하게 통과했는데 apply가 중간에 죽는 상황은 테라폼 운영에서 가장 당황스러운 순간입니다. 결론을 먼저 적으면, plan은 시뮬레이션이고 apply는 실제 API 호출이라 검증 범위 자체가 다르며, 실패 후에 가장 먼저 할 일은 에러 수정이 아니라 “어디까지 만들어졌는가"의 확인입니다. apply는 트랜잭션이 아니어서 실패 시점까지 만든 리소스는 그대로 남고, 그 상태를 잘못 만지면 고아 리소스와 state 불일치가 생깁니다. plan이 변경을 계산하는 원리는 기초 #5 state의 정체에서 다뤘습니다.

plan이 못 잡는 것: 검증 범위의 비대칭 #

plan이 검증하는 것은 구성 문법, 타입, 리소스 간 참조, 그리고 현재 state와의 차이입니다. provider가 로컬에서 아는 범위의 스키마 검증도 포함됩니다. 반면 AWS 쪽에서만 판정할 수 있는 것들은 apply에서 실제 API를 부를 때 처음 검증됩니다. 이 틈에서 나오는 전형 원인이 다섯 가지입니다.

유형전형 에러plan이 못 잡는 이유
권한 부족AccessDenied, UnauthorizedOperationplan은 읽기 권한만 쓰므로 쓰기 권한 구멍이 안 드러남
IAM 전파 지연InvalidParameterValue (role not found 류)방금 만든 역할·정책이 다른 서비스에 아직 전파 전
서비스 쿼터LimitExceeded, AddressLimitExceeded한도는 API가 생성 시점에 판정
전역 이름 충돌BucketAlreadyExists, already in use다른 계정까지 포함한 유일성은 생성 시점에만 확인 가능
파라미터 조합Unsupported, InvalidParameterCombination특정 AZ의 인스턴스 타입 미지원 같은 서버 측 규칙

첫 번째 유형이 실무에서 가장 흔합니다. CI에서 plan 잡은 읽기 전용 역할, apply 잡은 쓰기 역할로 분리하는 구성이 표준인데, apply 역할에 새 리소스 타입의 권한을 추가하는 것을 잊으면 정확히 이 패턴이 됩니다. 두 번째 유형은 IAM 역할을 만들고 같은 apply 안에서 바로 참조할 때 나옵니다. provider가 대부분 재시도로 흡수하지만, 전파가 유난히 느리면 실패로 떨어집니다. 이 경우는 그냥 재실행이 정답입니다.

실패 직후: 상태 파악이 먼저입니다 #

apply가 리소스 10개 중 6번째에서 죽었다면, 1〜5번은 이미 만들어져 있고 state에도 기록되어 있습니다. 테라폼은 리소스 하나의 생성이 성공할 때마다 state를 갱신하므로, 대부분의 실패는 “성공한 것은 state에 있고, 실패한 것부터 없는” 깔끔한 상태로 남습니다. 이 경우 복구는 간단합니다.

  1. 에러 메시지로 원인을 수정합니다: 권한 추가, 쿼터 증설 요청, 이름 변경 등 원인 유형에 맞는 조치를 합니다.
  2. plan을 다시 실행합니다: 이미 만들어진 리소스는 변경 없음으로 나오고, 실패했던 것과 그 뒤 리소스만 생성 대상으로 나옵니다. 이 plan이 예상과 같은지 확인합니다.
  3. apply를 다시 실행합니다: 남은 것만 만들어지며 원래 목표 상태에 도달합니다. 처음부터 다시 만들 필요도, 만들어진 것을 지울 필요도 없습니다.

주의할 것은 이 사이에 다른 조치를 하고 싶은 유혹입니다. 실패했다고 콘솔에서 리소스를 손으로 만들어 채우면 다음 apply에서 이름 충돌이 나고, 만들어진 리소스를 콘솔에서 지우면 state와 실물이 어긋나 refresh 때 혼란이 커집니다. 복구의 기본은 원인 수정 후 재실행이고, 콘솔 개입은 마지막 수단입니다.

“이미 존재합니다” 에러: state에 없는데 실물이 있는 경우 #

재실행했는데 BucketAlreadyExists나 already exists 류의 에러가 나면 더 성가신 케이스입니다. API 호출은 성공해서 실물이 만들어졌는데, 응답을 받기 전에 프로세스가 죽는 등의 이유로 state 기록만 누락된 상태입니다. 네트워크 단절이나 CI 잡 강제 종료 직후에 나옵니다.

선택지는 두 가지입니다. 실물을 코드로 편입하려면 import 블록으로 state에 붙입니다. 실물이 불완전하게 만들어졌다고 판단되면 콘솔이나 CLI로 지우고 apply로 다시 만듭니다. 어느 쪽이든 “state와 실물을 다시 일치시킨다"가 목표이고, 판단 기준은 그 리소스가 생성 후 데이터를 가졌는가입니다. 빈 리소스면 지우고 재생성이 단순하고, 데이터가 이미 들어갔으면 import가 안전합니다. state 수술 도구 전반은 운영 #2 drift와 state 수술에서 다뤘습니다.

-target으로 특정 리소스만 골라 적용하는 방법은 이 국면에서 자주 권해지지만, 부분 적용 상태를 하나 더 만드는 도구이기도 합니다. 의존성 일부만 적용된 스택은 다음 사람이 이해하기 어려우므로, 전체 plan이 깨끗해지는 방향으로 복구하고 -target은 격리가 꼭 필요한 예외에만 씁니다.

재발 방지 #

  • apply 역할의 권한을 릴리스 절차에 넣습니다: 새 리소스 타입을 도입하는 PR은 CI apply 역할의 정책 갱신을 함께 검토합니다. plan 통과가 권한 검증이 아니라는 사실을 팀 공통 인식으로 만듭니다.
  • 쿼터를 미리 봅니다: VPC, EIP, EC2 vCPU처럼 걸리기 쉬운 한도는 Service Quotas 콘솔에서 확인하고, 대량 생성 전에 증설을 신청해 둡니다.
  • 전역 유일 이름에는 접미사를 붙입니다: S3 버킷 등은 계정 ID나 랜덤 접미사를 관례로 넣어 충돌 자체를 없앱니다.
  • CI 잡의 강제 종료를 줄입니다: 타임아웃을 apply 소요보다 넉넉히 잡고, 잡 취소 대신 완주 후 되돌리는 운영이 state 누락 사고를 줄입니다.

정리 #

  • plan은 스키마·참조·state 차이만 검증하고, 권한·쿼터·이름 유일성·파라미터 조합은 apply의 실제 API 호출에서 처음 판정됩니다.
  • apply는 트랜잭션이 아니지만, 성공한 리소스는 state에 기록되므로 대부분의 실패는 원인 수정 후 재실행만으로 복구됩니다.
  • 실패 직후 콘솔에서 손대는 것이 사태를 키웁니다. 재실행 plan이 예상대로인지 확인하는 것이 복구의 중심입니다.
  • already exists 에러는 state 기록 누락 신호입니다. 데이터가 있으면 import, 빈 리소스면 삭제 후 재생성으로 state와 실물을 다시 맞춥니다.
  • plan 통과를 권한 검증으로 오해하지 않는 것, 쿼터·이름 관례를 미리 정리하는 것이 이 사고의 대부분을 예방합니다.
X