terraform destroy가 실패할 때: DependencyViolation과 지워지지 않는 리소스
실험 스택을 지우려고 terraform destroy를 돌렸는데 중간에 멈추는 일은 생각보다 흔합니다. 결론을 먼저 적으면, destroy 실패는 세 갈래입니다. 테라폼 밖에서 만든 것이 물고 있거나(DependencyViolation), 리소스가 비어 있지 않거나, 일부러 걸어 둔 안전장치가 작동한 것입니다. 각각 진단 위치와 해제 절차가 다르고, 특히 안전장치 해제는 “코드 수정 후 apply를 먼저” 해야 한다는 순서가 있습니다. 안전장치를 거는 쪽의 설계는 기초 #7 리소스 수명주기 제어에서 다뤘고, 이 글은 걸린 것을 푸는 쪽입니다.
한눈에 보는 실패 유형 #
| 유형 | 전형 에러 | 원인 | 해법 |
|---|---|---|---|
| 외부 의존물 잔존 | DependencyViolation | 테라폼 밖에서 만든 리소스가 참조 중 | 의존물을 찾아 먼저 제거 |
| 비어 있지 않음 | BucketNotEmpty 등 | S3 객체, ECR 이미지 잔존 | force_destroy 설정 후 apply → destroy |
| 안전장치 | lifecycle 에러, deletion protection | prevent_destroy, deletion_protection | 코드에서 해제 → apply → destroy |
| 삭제 지연 | timeout while waiting | CloudFront 등 삭제가 원래 느림 | timeouts 조정, 재실행 |
| 부분 destroy 잔존 | 재실행 시 남은 것만 | 앞선 실패로 일부만 삭제됨 | destroy 재실행이 기본 |
DependencyViolation: 범인은 테라폼 밖에 있습니다 #
보안 그룹이나 VPC를 지우다 나오는 DependencyViolation은 destroy 실패의 대표입니다. 테라폼은 자기가 아는 의존성 순서대로 지우므로, 테라폼이 관리하는 리소스끼리라면 이 에러가 나지 않습니다. 에러가 났다는 것은 테라폼이 모르는 무언가가 그 리소스를 물고 있다는 뜻입니다.
전형적인 범인은 이렇습니다. 콘솔에서 수동으로 띄운 EC2 인스턴스가 테라폼 관리 보안 그룹을 쓰고 있는 경우, Lambda나 ALB가 만든 ENI가 서브넷에 남아 있는 경우, 다른 스택이나 다른 팀의 리소스가 이 VPC 안에 만들어진 경우입니다. 진단은 대상별로 위치가 정해져 있습니다. 보안 그룹이면 EC2 콘솔에서 해당 SG를 참조하는 ENI를 검색하고, 서브넷이면 그 서브넷의 ENI 목록을 보고, VPC면 VPC 콘솔의 리소스 맵에서 남은 것을 확인합니다. CLI라면 aws ec2 describe-network-interfaces --filters로 같은 조회가 됩니다.
의존물을 찾았으면 그것부터 지우고 destroy를 재실행합니다. 주의할 점은 그 의존물이 다른 팀의 실물일 수 있다는 것입니다. 지우기 전에 소유자를 확인하는 절차는 생략하면 안 됩니다.
비어 있지 않은 리소스: force_destroy는 apply가 먼저입니다 #
S3 버킷의 BucketNotEmpty는 두 번째로 흔한 실패입니다. 테라폼은 기본적으로 객체가 든 버킷을 지우지 않습니다. force_destroy = true를 설정하면 객체까지 지우고 버킷을 삭제하는데, 여기에 순서 함정이 있습니다. 이 인수는 state에 반영되어 있어야 작동하므로, 코드에 쓰고 apply를 한 번 실행한 다음에 destroy를 해야 합니다. destroy 직전에 코드만 고치고 바로 destroy하면 이전 설정으로 동작해 같은 에러가 다시 납니다.
버저닝이 켜진 버킷은 모든 버전과 삭제 마커까지 지워야 하므로 객체가 많으면 오래 걸립니다. 수명 주기 규칙으로 미리 비우거나, 대량이면 aws s3 rm --recursive로 먼저 비우는 쪽이 빠를 때도 있습니다. ECR 리포지토리의 이미지 잔존도 같은 구조이고 force_delete = true가 대응 인수입니다.
안전장치: 푸는 순서가 정해져 있습니다 #
prevent_destroy가 걸린 리소스는 destroy 계획 단계에서 “Instance cannot be destroyed” 에러로 멈춥니다. 이것은 사고가 아니라 설계 의도가 작동한 것이므로, 정말 지워야 하는지의 판단이 먼저입니다. 지우는 것이 맞다면 코드에서 prevent_destroy = true를 제거하거나 false로 바꾼 뒤 destroy를 실행합니다. lifecycle 설정은 구성 파일에서 읽히므로 이 경우는 apply 없이 destroy가 가능하지만, 코드 수정이 리뷰를 거치도록 PR로 남기는 것이 운영 원칙입니다.
RDS나 ALB의 deletion_protection은 층이 다릅니다. 이것은 AWS 리소스 자체의 속성이라 인수를 false로 바꾸고 apply로 실물에 반영한 다음에야 destroy가 통과합니다. RDS는 여기에 최종 스냅샷이 겹칩니다. skip_final_snapshot이 false(기본)면 final_snapshot_identifier가 없을 때 삭제가 실패하므로, 스냅샷을 남길지 결정하고 인수를 맞춘 뒤 apply → destroy 순서로 갑니다. 데이터 계층의 이런 안전장치 설계는 실전 #5 RDS와 S3에서 다룬 대로 일부러 만들어 둔 마찰이므로, 귀찮다고 기본값에서 꺼 두는 것은 방향이 반대입니다.
지연, 부분 실패, 그리고 잔존물 #
CloudFront 배포판이나 일부 글로벌 리소스는 삭제 자체가 수십 분 걸립니다. timeout 에러가 났다면 실제로는 삭제가 진행 중인 경우가 많아, 잠시 뒤 destroy를 재실행하면 이미 지워졌다는 확인과 함께 넘어갑니다. 상습적으로 걸리는 리소스에는 timeouts 블록으로 delete 시간을 늘려 둡니다.
destroy가 중간에 실패하면 지워진 것은 state에서 빠지고 남은 것만 남습니다. apply의 부분 적용과 마찬가지로 원인을 제거하고 destroy를 재실행하는 것이 기본 복구입니다. 드물게 실물은 지워졌는데 state에 남는 반대 케이스가 있는데, refresh가 사라진 리소스를 감지해 정리해 주고, 안 되면 removed 블록이나 terraform state rm으로 정리합니다. 이 도구들은 운영 #2 drift와 state 수술의 영역입니다.
정리 #
- destroy 실패는 외부 의존물, 비어 있지 않은 리소스, 안전장치의 세 갈래입니다. 에러 메시지로 갈래를 먼저 판정합니다.
- DependencyViolation은 테라폼 밖의 리소스가 물고 있다는 신호입니다. ENI·리소스 맵에서 의존물을 찾고, 소유자 확인 후 제거합니다.
- force_destroy와 deletion_protection 해제는 apply로 실물에 반영한 뒤에 destroy해야 합니다. 코드만 고치고 바로 destroy하면 같은 에러가 반복됩니다.
- prevent_destroy는 설계가 작동한 것입니다. 해제는 코드 수정으로 하되, 정말 지울 대상인지 판단이 먼저입니다.
- 타임아웃과 부분 실패는 재실행이 기본 복구입니다. 테라폼 관리 VPC 안에 수동 리소스를 만들지 않는 규율이 이 사고의 근본 예방입니다.