terraform destroy が失敗するとき — DependencyViolation と消えないリソース
実験用のスタックを消そうと terraform destroy を回したら途中で止まる、ということは思ったよりよくあります。結論を先に書くと、destroy の失敗は 3 つの枝に分かれます。Terraform の外で作られたものが掴んでいるか(DependencyViolation)、リソースが空になっていないか、わざと掛けておいた安全装置が作動したかです。 それぞれ診断する場所と解除の手順が違い、特に安全装置の解除には「コードを直して apply が先」という順序があります。安全装置を掛ける側の設計は基礎 #7 リソースライフサイクル制御で扱ったので、この記事は掛かったものを外す側です。
一目でわかる失敗類型 #
| 類型 | 典型エラー | 原因 | 対処 |
|---|---|---|---|
| 外部の依存物が残存 | DependencyViolation | Terraform の外で作られたリソースが参照中 | 依存物を探して先に除去 |
| 空になっていない | BucketNotEmpty など | S3 オブジェクト、ECR イメージが残存 | force_destroy 設定後 apply → destroy |
| 安全装置 | lifecycle エラー、deletion protection | prevent_destroy、deletion_protection | コードで解除 → apply → destroy |
| 削除の遅延 | timeout while waiting | CloudFront などは削除がもともと遅い | timeouts 調整、再実行 |
| 部分 destroy の残存 | 再実行時に残りだけ表示 | 先の失敗で一部だけ削除された | destroy の再実行が基本 |
DependencyViolation — 犯人は Terraform の外にいます #
セキュリティグループや VPC を消そうとして出る DependencyViolation は destroy 失敗の代表です。Terraform は自分が知っている依存の順序で消すため、Terraform 管理のリソース同士ならこのエラーは出ません。 出たということは、Terraform の知らない何かがそのリソースを掴んでいるという意味です。
典型的な犯人はこうです。コンソールで手動起動した EC2 インスタンスが Terraform 管理のセキュリティグループを使っている場合、Lambda や ALB が作った ENI がサブネットに残っている場合、別のスタックや別のチームのリソースがこの VPC の中に作られている場合です。診断は対象ごとに場所が決まっています。セキュリティグループなら EC2 コンソールでその SG を参照する ENI を検索し、サブネットならそのサブネットの ENI 一覧を見て、VPC なら VPC コンソールのリソースマップで残りを確認します。CLI なら aws ec2 describe-network-interfaces --filters で同じ照会ができます。
依存物が見つかったら、それを先に消して destroy を再実行します。注意すべきは、その依存物が別のチームの実物かもしれない点です。消す前に所有者を確認する手順は省略してはいけません。
空になっていないリソース — force_destroy は apply が先です #
S3 バケットの BucketNotEmpty は 2 番目によくある失敗です。Terraform はデフォルトではオブジェクトの入ったバケットを消しません。force_destroy = true を設定するとオブジェクトごとバケットを削除しますが、ここに順序の落とし穴があります。この引数は state に反映されていて初めて機能するため、コードに書いて apply を 1 回実行してから 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 の失敗は外部の依存物、空になっていないリソース、安全装置の 3 つの枝です。エラーメッセージで枝を先に判定します。
- DependencyViolation は Terraform の外のリソースが掴んでいる合図です。ENI・リソースマップで依存物を探し、所有者を確認してから除去します。
- force_destroy と deletion_protection の解除は、apply で実物に反映してから destroy します。コードだけ直してすぐ destroy すると同じエラーが繰り返されます。
- prevent_destroy は設計が作動したものです。解除はコード修正で行い、本当に消す対象かの判断が先です。
- タイムアウトと部分失敗は再実行が基本の復旧です。Terraform 管理の VPC の中に手動リソースを作らない規律が、この事故の根本的な予防です。