terraform plan は成功したのに apply が失敗するとき — 原因と部分適用の復旧

読了 5分

terraform plan はきれいに通ったのに apply が途中で止まる状況は、Terraform の運用で最も戸惑う瞬間です。結論を先に書くと、plan はシミュレーションで apply は実際の API 呼び出しなので検証範囲そのものが違い、失敗後に最初にやることはエラーの修正ではなく「どこまで作られたか」の確認です。 apply はトランザクションではないため、失敗時点までに作られたリソースはそのまま残り、その状態の扱いを誤ると孤児リソースと state の不整合が生まれます。plan が変更を計算する原理は基礎 #5 state の正体で扱いました。

plan が捕まえられないもの — 検証範囲の非対称 #

plan が検証するのは構成の文法、型、リソース間の参照、そして現在の state との差分です。provider がローカルで知っている範囲のスキーマ検証も含まれます。一方、AWS 側でしか判定できないものは、apply が実際の API を呼ぶときに初めて検証されます。 この隙間から出てくる典型原因が 5 つです。

類型典型エラーplan が捕まえられない理由
権限不足AccessDenied、UnauthorizedOperationplan は読み取り権限しか使わず、書き込み権限の穴が現れない
IAM 伝播遅延InvalidParameterValue(role not found 系)作ったばかりのロール・ポリシーが他サービスに伝播する前
サービスクォータLimitExceeded、AddressLimitExceeded上限は API が作成時点で判定する
グローバル名の衝突BucketAlreadyExists、already in use他アカウントまで含む一意性は作成時点でしか確認できない
パラメータの組み合わせUnsupported、InvalidParameterCombination特定 AZ でのインスタンスタイプ非対応のようなサーバー側ルール

1 つ目の類型が実務で最も多いです。CI で plan 用の読み取り専用ロールと apply 用の書き込みロールを分ける構成が標準ですが、新しいリソースタイプを導入するとき apply ロールへの権限追加を忘れると、ちょうどこのパターンになります。2 つ目は IAM ロールを作って同じ apply の中ですぐ参照するときに出ます。provider が大部分をリトライで吸収しますが、伝播が特別に遅いと失敗に落ちます。この場合は再実行が正解です。

失敗直後 — 状態の把握が先です #

apply がリソース 10 個のうち 6 個目で止まったなら、1〜5 個目はすでに作られていて state にも記録されています。Terraform はリソース 1 つの作成が成功するたびに state を更新するため、大部分の失敗は「成功したものは state にあり、失敗したものからはない」という切れ目のよい状態で残ります。 この場合の復旧は単純です。

  1. エラーメッセージの原因を修正します: 権限の追加、クォータの引き上げ申請、名前の変更など、原因の類型に合った対処をします。
  2. plan を再実行します: すでに作られたリソースは変更なしで表示され、失敗したものとその後のリソースだけが作成対象に出ます。この plan が想定どおりかを確認します。
  3. apply を再実行します: 残りだけが作られ、元の目標状態に到達します。最初から作り直す必要も、作られたものを消す必要もありません。

注意すべきは、この間に別の手を打ちたくなる誘惑です。失敗したからとコンソールでリソースを手作りして埋めると次の apply で名前衝突が起き、作られたリソースをコンソールで消すと state と実物がずれて refresh 時の混乱が大きくなります。復旧の基本は原因修正後の再実行で、コンソールでの介入は最後の手段です。

already exists エラー — state にないのに実物がある場合 #

再実行したのに BucketAlreadyExists や already exists 系のエラーが出たら、より厄介なケースです。API 呼び出しは成功して実物が作られたのに、応答を受け取る前にプロセスが死ぬなどの理由で state の記録だけが欠けた状態です。ネットワーク断や CI ジョブの強制終了の直後に出ます。

選択肢は 2 つです。実物をコードに取り込むなら import ブロックで state に付けます。実物が不完全に作られたと判断するなら、コンソールや CLI で消して apply で作り直します。どちらでも目標は「state と実物を再び一致させる」ことで、判断基準はそのリソースが作成後にデータを持ったかどうかです。空のリソースなら消して再作成が単純で、データがすでに入ったなら import が安全です。state 手術の道具全般は運用 #2 drift と state 手術で扱いました。

-target で特定リソースだけを選んで適用する方法はこの局面でよく勧められますが、部分適用状態をもう 1 つ作る道具でもあります。依存関係の一部だけが適用されたスタックは次の人が理解しにくいため、全体の 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