TL;DR
- terraform stateがローカルにバラついていたので、共有のS3バックエンドへ統一しました
- そのうち1領域は、stateの正本が1台のノートPCにしかなく、しかもstateの中に稼働中インフラの秘密値が入っていました
- なので「もう一度importして作り直せばいい」を拒否しました。importで作り直すと秘密値が再生成され、稼働中のトンネルが壊れます
- 移行の成功条件を
terraform init -migrate-stateが成功したことに置きませんでした。43リソースのアドレスと全インスタンス属性を移行前後で比較して一致を確認し、秘密値の一致を個別に確認し、そのうえでplan = No changesを見て初めて成功としました -
lineageとserialは移行時に再採番されます。これは仕様であり、リソース内容が一致していれば機能影響はありません。「値が変わった」と「意味が変わった」を分けて判定する必要があります - 結論: Command success ≠ semantic equivalence(コマンドの成功と、意味の同一性は別)
観測日: 2026-06-24 〜 2026-07-03
はじめに
連載の最終回です。1本目に書いた工程のうち、「実環境で確認する」の段をいちばん丁寧にやった作業がこれでした。
一言で言うと、この記事の主題はこれです。
「コマンドが成功した」と「移行が正しかった」は違う。
前作の記事に書いた「テストが緑」と「本番で正しい」は違う、と同じ思想です。インフラ側でこれが最も鮮明に出るのが、stateの移行だと思っています。
1. なぜ移行したか
stateの運用が領域ごとにバラついていました。
- ある領域は、リポジトリの別の場所にあるstateファイルを毎回コマンドラインオプションで指す変則運用
- 別の領域は、各自のローカルの既定位置
- いずれもバージョン管理の対象外なので、マシン間で同期されず、履歴も残らない
この状態の問題は3つあります。
| 問題 | 内容 |
|---|---|
| 操作が均等でない | 領域ごとに手順が違う。手順が違うと、間違った手順を使う事故が起きる |
| マシン間で同期されない | 別のマシンで作業すると、そのマシンのstateが古いか無い |
| 証跡が残らない | 誰がいつどう変えたかが追えない |
共有のS3リモートバックエンドへ統一すれば、3つとも解決します。正本がS3の1箇所になり、どの領域も同じコマンドで動き、バージョニングで履歴が残ります。
設定自体は短いものです。
terraform {
backend "s3" {
bucket = "<state用バケット>"
key = "<領域>/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true
}
}
use_lockfile = true は Terraform 1.10 以降のS3ネイティブロックです。以前はロック用に別のテーブルを用意する必要がありましたが、今は要りません。実機で確認して採用しました。
2. 1台のノートPCにしかないstate
4領域あって、3つは素直に移りました。問題は4つ目でした。
調査して分かったことを並べます。
- プライマリの作業マシンに、その領域のstateが存在しない。providerのキャッシュはあるが、state本体が無い
- 実リソースは40件以上ある(DNS、ゼロトラスト接続、トンネル等)。誰かがどこかでapplyしている
- つまり正本は別のノートPCにしかない
さらに悪いことが分かりました。
stateの中に、稼働中インフラの秘密値が入っていました。
具体的には、トンネルの認証に使うランダム値がTerraformのリソースとして生成されていて、その値がstateにしか存在しません。コードには「このランダム値を使う」と書いてあるだけで、値そのものはstateの中です。
これが決定的でした。
3. 「importして作り直す」を拒否した
stateが無い場所でTerraformを回す方法として、まず思いつくのは2つです。
- 空のstateから
terraform importで実リソースを取り込んで作り直す - 何もimportせず、planに出た差分をapplyして実態に合わせる
どちらも不可でした。
2番は論外です。空のstateでplanを取ると、Terraformは「全部これから作る」と判断します。実際には既にあるものを「作る」とapplyすれば、衝突するか二重に作られます。
1番も不可でした。理由は秘密値です。
import で取り込めるのは、APIから読めるものだけ。
トンネルの認証に使うランダム値は、
Terraform が生成して state に保持しているもので、
API からは読めない。
import して作り直すと、その値は「再生成」される。
→ 稼働中のトンネルが壊れる。
Terraformのランダム値生成リソースは、値をstateに保持します。stateを作り直せば新しい値が生成され、その新しい値でトンネルが再構築されます。つまり現に動いている接続が切れます。
したがって結論はこうなりました。
空移行も import 再構築もしない。
正本の state そのものを、正本がある端末から S3 へ移す。
これが唯一安全な経路である。
「作り直せばいい」を拒否したのが、この作業でいちばん重要な判断でした。stateは単なるキャッシュではなく、そこにしか無い情報を持ちうるデータです。 これを理解していないと、平気で作り直そうとします。
そして正直に書くと、この構造に気づいたのは調査の途中です。最初は「stateが無いならimportで作ればいい」と考えていました。コードを読んで、トンネルの認証値がTerraform生成のランダム値であることに気づいて、方針が変わりました。
4. 移行前のプリフライト
移行の前に、毎回実行する確認手順を作りました。独立レビュー(codex)の指摘を反映して6項目あります。
- Terraformのバージョン が要件(1.10以上)を満たすこと
- 実行しているIAMプリンシパルの確認。stateバケットはアクセスを絞っているので、想定の主体でないとロックアウトする
- stateバケットへの実効権限。バケットの一覧取得、stateキーの読み書き、そしてロックファイルの削除権限(ロック解放に必須)
- 移行先キーの空き確認。既存のstateや残留したロックファイルが無いこと
-
正本のバックアップ。
terraform state pullでファイルに保存し、lineage/serial/ リソース数を控える(移行後に照合するため) - メンテナンスウィンドウ。移行中は他マシンや旧手順からのapplyを禁止する(stateの分岐を防ぐ)
3番の「ロックファイルの削除権限」は、独立レビューで指摘されて足りました。読み書きだけ確認していると、ロックは取れるが解放できない状態になります。次のplanが永久にブロックされます。
5番が、この記事の主題に直結します。移行後に照合するための証拠を、移行前に取っておく ということです。これをやらないと、移行後に「たぶん合ってる」しか言えません。
5. 何をもって成功としたか
これが本題です。
移行のコマンド自体は1行です。
terraform init -migrate-state
対話プロンプトに yes と答えると、Terraformがローカルのstateをリモートへコピーします。そして成功終了します。
この成功終了を、移行の成功条件にしませんでした。
init -migrate-state が成功したというのは、「コピー操作がエラーを返さなかった」という意味です。それが「移行前後で意味が同一である」ことを意味するかは、別の問題です。
確認したのは次です。
5.1 リソースの完全一致
移行後のS3上のstateについて、43リソースのアドレスと全インスタンス属性が、移行前とバイト同一 であることを確認しました。差分0です。
移行前に取っておいたバックアップ(プリフライト5番)と、移行後の terraform state pull を機械的に比較しています。目視ではありません。
リソース数が合っているだけでは足りません。43件が43件でも、中の属性が変わっていたら意味が違います。アドレス(resource_type.name の並び)と属性の両方を見ます。
5.2 秘密値の個別確認
トンネルの認証に使うランダム値について、エンコード形式ごとに値が一致していること を個別に確認しました。
これは5.1に含まれています(属性の一部なので)。それでも別立てで確認したのは、この値の一致が「稼働中のトンネルが無傷である」ことの直接の根拠だからです。
43件全部が一致した、という総合的な事実と、この1つの値が一致した、という個別の事実は、証拠としての役割が違います。 前者は「移行が正しかった」の根拠で、後者は「サービスが壊れていない」の根拠です。
5.3 plan = No changes
そのうえで terraform plan を取り、No changes を確認しました。
これは1本目に書いた「再plan = no-op」の段です。コードとstateと実インフラの3つが一致したことを、人間の目視ではなくTerraformの差分検出器そのものに言わせています。
5.4 lineage と serial は一致しない。それでよい
ここが判定の面白いところでした。
移行後、lineage と serial は変わっていました。
lineage: <移行前のID> → <別のID>
serial: 129 → 1
数値が変わっているので、素朴に見ると「一致しなかった」と判定しそうになります。
しかしこれは仕様です。lineage はstateの系統を識別するIDで、serial は更新のカウンタです。バックエンドを移すと新しい系統として採番されます。リソースの内容とは無関係です。
つまり判定を、
移行前後で state ファイルが同一か
ではなく、
移行前後でリソースのアドレスと属性が同一か
(lineage / serial の再採番は仕様として除外する)
に置く必要があります。
「値が変わった」と「意味が変わった」を分けて判定する ということです。前者だけを見ると偽の異常を検出しますし、逆にプリフライトで lineage/serial を控えた目的を「一致確認」だと誤解すると、この段で混乱します。控えた目的は、同じ系統のstateを見ているかの確認(移行元を取り違えていないか)であって、移行後の一致確認ではありません。
これは品質判定の設計として一般的な話だと思っています。比較する対象を決めるときに、意味に関係する部分と、実装上のメタデータを分ける。分けずに全体をdiffすると、ノイズで本当の差分が埋もれるか、偽陽性で判定が止まります。
6. 途中で見つかった別の穴
移行のついでに1つ見つかりました。
その領域の暗号化されたsecretファイルが、2つの受信者鍵のうち1つにしか暗号化されていませんでした。つまり、正本のstateを持つノートPCでは復号できるが、もう1台では復号できない。
これも「動いていたから気づかなかった」類です。正本を持つ端末で作業している限り、復号できるので問題が現れません。もう1台で作業しようとした瞬間に止まります。
移行と同時に再暗号化して、両方の端末で使えるようにしました。
1台にしか無いものは、その1台で作業している限り可視化されません。 stateも、復号鍵も、同じ構造でした。移行という作業は、この種の「1台依存」を強制的に洗い出します。
7. ロールバックの設計
移行のような不可逆に見える作業では、戻せる経路を先に決めておきました。
-
init -migrate-stateは元のローカルstateを消しません。バックアップが残ります。したがって、No changesと一致確認が済むまでローカルの実体を削除しません - stateバケットはバージョニングを有効にしているので、誤上書きも版から復旧できます。オブジェクトのバージョン一覧からIDを特定して取得します
- ロックが残った場合は、Terraformのロック解除コマンドで解放します。復旧後に一致確認をやり直します
- バケット自体は、削除保護の設定を入れています。stateを置く先を誤って消せる状態にはしません
そして順序として決めたのは、「一致確認が終わるまで移行元を消さない」 です。
移行が成功したように見えた時点でローカルを消してしまうと、確認の途中で異常が見つかっても戻れません。確認と後片付けを分けて、確認が全部終わってから片付けます。
8. 移行して初めて見えた設計課題
最後に、移行の結果として浮上した判断について書きます。技術ではなく設計の話です。
共有バックエンドにした目的のひとつは、「運用者以外のメンバーもTerraformを回せるようにする」ことでした。しかし現状、stateバケットへのアクセスは経営ボードに限っています。したがって一般メンバーがTerraformを実行しても、stateバケットへのアクセスが拒否されて動きません。
ここで気づいたのは、この2つが分離できないことです。
「誰に IaC 実行権を渡すか」
「state の機微をどこまで共有するか」
この2つはセットで判断するしかない。
stateには機微情報が入ります。この記事で書いたトンネルの認証値がまさにそれです。state読み取り権を渡すというのは、それらを見せるということです。
Terraformを実行できるようにするために state 読み取り権を渡すと、意図せず秘密値の閲覧権も渡すことになります。ここは領域ごとにバケットやアクセス制御を分ける、という代替案を検討事項として残しています。
「stateはただの内部ファイル」と思っていると、この判断を見落とします。 stateは、機微情報を含みうる成果物です。
まとめ
- Command success ≠ semantic equivalence。 コマンドが成功したことは、意味が同一であることを意味しない
- stateは単なるキャッシュではない。そこにしか無い情報を持ちうる
- Terraformが生成したランダム値は、importで復元できない。作り直すと再生成され、稼働中のリソースが壊れる
- 「作り直せばいい」を拒否する判断が、この作業の中心だった
- 移行後に照合するための証拠を、移行前に取っておく
- リソース数の一致では足りない。アドレスと全属性を機械的に比較する
- 総合的な一致(43件同一)と、個別の一致(この秘密値が同一)は証拠としての役割が違う
-
lineage/serialの再採番は仕様。「値が変わった」と「意味が変わった」を分けて判定する - ロックの取得権限だけでなく、解放(削除)の権限も事前に確認する
- 一致確認が終わるまで移行元を消さない。確認と後片付けを分ける
- 1台にしか無いものは、その1台で作業している限り可視化されない
- stateの読み取り権は、機微情報の閲覧権とセットで判断する
連載のまとめ
5本を通して書いてきたことを、最後にまとめます。
Terraformをほぼ知らない人間が、LLMに書かせたインフラコードを本番にapplyしています。それを正当化しているのは、コードを理解したことではなく、性質の違う証拠を順に積む工程です。
そして5本のうち4本は、その工程が破れた(または破れかけた)話でした。
| 破れ方 | どこで |
|---|---|
| 設定が存在するのに関門が作動していなかった | 2本目 |
| planのサマリで判断すると、無関係なdriftを巻き込む | 3本目 |
| 宣言的に書いたものが排他ではなかった | 4本目 |
| コマンドの成功を、意味の同一性と混同しかけた | 5本目 |
共通しているのは、「確認したつもり」の形が毎回違う ことです。だから単一の強い関門ではなく、見落としの方向が違う関門を重ねる形になります。
前作のアプリケーション側の記事と同じ結論に着地します。
LLM を信用しない。
LLM が作ったものを反証可能にする工程を信用する。
LLMがインフラのコードを書ける時代に、人間の側に残るのはコードを書くことではありません。証拠が足りているかを判断すること です。そして足りていないと分かったときに、押し通さずに止まることです。
七夕研究所ではまだこの方法を作っている途中です。研究的に新しいことをしているつもりはなく、既存の部品を運用に耐える形へ組み直しているだけです。
この記事は 七夕研究所 Qiita Organization の記事です。開発と運用で実際に踏んだものを順次書いています。