第7回でデータの置き場所が決まりました。次に決めるのは、ここまでの設計をどうコードとして管理するかです。
第7回で1つ、宿題を残しました。GKEクラスタの構成情報は、Backup for GKE のバックアップ対象外です。ノード構成、ノードプール、初期クラスタサイズ、有効な機能。これらは取得されません。
つまり、クラスタを失ったときに戻せるかどうかは、バックアップではなくIaCの品質にかかっています。本記事はここから始めます。
扱う範囲を明確にしておきます。本記事はクラウドリソースを作るコード、つまりTerraformなどのインフラコードの話です。Kubernetesマニフェストのリポジトリ構成は第10回の管轄です。「リポジトリ構成」という語が両方にまたがるため、この線引きを設計書でも守ってください。
目次
IaCの完了条件は「applyが通ること」ではない
まず完了条件を置き直します。
「空のプロジェクトから再現できること」を完了条件にする
applyが通ることは、コードが現在の状態と矛盾していないことを示すだけです。ゼロから作り直せるかどうかは、まったく別の性質です。
差分適用を繰り返してきたコードは、既存リソースの存在を暗黙の前提にしていることがあります。手で作ったネットワーク、先に存在していたサービスアカウント、誰かが一度だけコンソールで有効化したAPI。これらはapplyでは表面化せず、空のプロジェクトで初めて失敗します。
したがって、完了条件をこう置きます。
「applyが通ること」ではなく「空のプロジェクトから再現できること」。
第7回でバックアップの完了条件を「取得できたこと」ではなく「復元テストに成功したこと」に置きました。同じ形です。前者は設定した事実、後者は意図どおりに動いている事実です。そして第7回で確認したとおり、クラスタ構成についてはIaCが復元手段そのものなので、この検証を通っていないIaCはバックアップの代わりになりません。
検証の周期も、第7回と同じ考え方で決めます。日数で置かず、「再現手順が壊れる要因が発生したか」で判断してください。
- クラスタのバージョンを上げたとき(第14回)
- モジュールの構造を変えたとき
- これまで扱っていなかった種類のリソースを追加したとき
- 手動で作ったリソースをコードに取り込んだとき
実施の範囲についても、現実的な線を引いてください。本番と同じ規模を作る必要はありません。第3回で「ミラーの定義は構成の同一性であって規模の同一性ではない」と確認したのと同じ考え方で、再現テストで確認するのは構成が立ち上がることであって、性能が出ることではありません。ノード数を最小にし、確認が終わったら削除する運用で十分です。
それでも作れないものは残ります。組織レベルのポリシー、他部門が管理するDNSゾーン、外部サービスとの接続設定。これらは「再現できない前提条件」として設計書に明記してください。再現テストの目的は100%を目指すことではなく、再現できない範囲を既知にすることです。
費用も設計に含めてください。再現テストは環境を1つ作る作業なので、第4回で算出したK8s税が、その時間分だけ追加で発生します。クラスタ管理手数料は1時間あたりで課金されるため、数時間で削除すれば大きな額にはなりません。ただし「作ったまま消し忘れる」と月額がそのまま乗ります。
したがって、再現テストの手順には削除まで含めてください。そして削除もコードで行います。作れることと消せることは別の性質で、消せないIaCは、テスト環境を作るたびに負債を増やします。第7回でPVCの棚卸しについて書いたのと同じ話が、ここでも起きます。
手で作ったリソースをどう扱うか
完全にIaCで管理されている環境は稀です。現実には、手で作られたリソースが必ず混ざります。
ただし、問題は手動リソースが存在することではありません。どれが手動なのかを誰も把握していないことです。
前者は既知の負債ですが、後者は再現テストで初めて発覚します。そして発覚するのは、たいてい本番を作り直そうとしている最中です。
対処は3手順です。
- IaC管理外のリソースを一覧化する
- それぞれについて「コードに取り込む」か「意図的に管理外とする」かを決める
- 管理外とするものには、その理由を書く
3番目が重要です。第6回でADRについて「空欄にしない」と書いたのと同じ考え方で、判断していないことと、判断したうえで除外したことを区別できる状態にします。「いつか取り込む」も、管理外の記録として扱ってください。記録がなければ、次に見る人はそれが漏れなのか意図なのか判断できません。
tfstateは第5回の設計の前提を崩す
ここが本記事で最も見落とされる点です。
stateにはシークレットが平文で入る
Terraformの公式ドキュメントは明記しています。「Terraform stores those secrets in its state and plan files」——シークレットはstateファイルとplanファイルに保存されます。ローカル環境については「Terraform stores your state in a plaintext file, which includes any secret values」と書かれています。
ここで第5回を思い出してください。第5回では、K8s Secretを避ける理由を「Base64は暗号化ではないから」ではないと説明しました。本質は、Secretの読み取り可否がKubernetes APIの権限で決まってしまうことでした。だから「Kubernetes APIを経由せずにPodへ渡す」という解に至りました。
ところがtfstateでは、読み取り可否がCloud IAMで決まります。
主体も監査経路も違います。第5回で作ったRBACマトリクスでは、この経路を制御できません。層4の設計は、IaC側で別途守る必要があります。「K8s Secretに実体を置かない」と決めた設計が、stateバケットの権限設定が緩いというだけで成立しなくなります。
具体的に何が入るかを挙げます。データベースのパスワード、Terraformが生成した鍵、そしてSecret Managerから読み出した値を別のリソースに渡した場合の、その値です。
最後のものに注意してください。「Secret Managerに置いたから安全」は、Terraformがその値を読んで別のリソースに渡した時点で崩れます。置き場所を正しくしても、経路が増えれば漏れます。
planファイルにも入る
先ほど引用した原文をもう一度読んでください。「its state and plan files」——stateファイルだけではありません。planファイルにも入ります。
これは本記事の後半で扱う設計と正面から衝突します。planの結果をプルリクエストにコメントとして出すという、レビューのための一般的な構成です。
つまり、対策を打たなければシークレットがプルリクエストのコメントとして残り、リポジトリを読める全員が見られる状態になります。stateバケットの権限を厳密に設計しても、この経路は塞がりません。そしてプルリクエストのコメントは、stateバケットよりはるかに多くの人が読めます。
設計時に確認すべきことは3つです。
- planの出力に機微な値が含まれていないかを、実際の出力で確認する。推測しない
- 含まれる場合、その値をTerraformに通さない設計(後述)に変えられないかを先に検討する
- 変えられない場合、planの出力をPRに出す構成そのものを見直す。レビューの利便性とシークレットの露出は、後者が優先します
この確認は、planをPRに出す仕組みを作る前に行ってください。作ってから気づくと、それまでのプルリクエストすべてが対象になります。
公式が推奨する4つの対策
Terraformの公式ドキュメントが挙げる対策は4つです。
- stateをリモートに保存する
- 保存時に暗号化する
- アクセス制御で、stateを読める人を絞る
- 監査ログでstateへのアクセスを追跡する
GCSバックエンドでの実装を整理します。
| 項目 | 内容 |
|---|---|
| state locking | 対応している |
| バージョニング | バケットでオブジェクトのバージョニングを有効にすることが強く推奨されている |
| 暗号化(顧客提供鍵) | encryption_key。32バイトのbase64エンコード。鍵を紛失するとデータを復旧できない |
| 暗号化(Cloud KMS) | kms_encryption_key。形式は projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{name} |
| 必要なIAM | Storage Object Admin ロール |
| state分離 | prefix でバケット内のパスを分けられる |
暗号化の2方式のうち、顧客提供鍵は鍵の紛失がそのままデータの喪失になります。第5回でCloud KMSのアプリケーションレイヤ暗号化について「使用中の鍵バージョンを破棄するとクラスタが動作しなくなる」と確認しましたが、ここでも同じ性質の危険があります。鍵の管理を自分たちで負えるかを判断してください。負えないなら Cloud KMS を選びます。
そして設計書に書くべきことが1つあります。stateバケットの読み取り権限を持つ主体の一覧です。第5回でRBACマトリクスを作ったのと同じ形式で、Cloud IAM側のマトリクスを作ってください。第5回のマトリクスだけでは、シークレットへの経路が1つ残ります。
そもそもstateに通さない
最も確実な対策は、機微な値をTerraformに通さないことです。
Terraformはシークレットの「入れ物」を作り、値そのものは別の経路で入れる。第5回で決めた Secret Manager アドオンの構成なら、Terraformが作るのはSecret Managerのシークレットリソースと、それを読むための権限です。値そのものは含みません。
この線引きには設計上の意味があります。「Terraformで何を作り、何を作らないか」が、シークレット設計の一部になります。IaCの網羅率を上げること自体は目的ではありません。網羅率を上げた結果として平文の値が増えるなら、それは設計の後退です。

マネージドのTerraformを先に検討する
第6回で確立した原則をここでも適用します。マネージドの選択肢があるなら、それを却下する理由が書けない限り選びません。
Google Cloud には Infrastructure Manager があります。「Google Cloudのインフラストラクチャリソースのデプロイと管理を簡素化・自動化するマネージドサービス」と定義されています。
設計上重要なのは、何を保存してくれるかです。リビジョンごとにログ、使用した構成、作成されたGoogle Cloudリソースの一覧、そして各デプロイメントとリビジョンで作られたstateファイルを保存します。実行環境はエフェメラルなCloud Build環境で、Terraformコマンドを実行した後に破棄されます。デプロイメントごとに、使用するTerraformのバージョンを指定できます。
前章で挙げた4つの対策のうち、「リモートに保存する」と「保存時に暗号化する」の設計負担が減ります。stateバケットの設計、バージョニングの有効化、暗号化方式の選択を自分でやらずに済みます。
- 既に別クラウドや自社データセンターで同じ仕組みを運用しており、統一したほうが総コストが下がる
- 必要な機能がマネージド版に無く、その機能が要件として確定している
- マネージド版の変更管理を受け入れられない規制要件がある
却下する場合、次の作業をすべて自前で設計することになります。却下するという判断は、この一覧を引き受けるという意味です。
- stateバケットの作成と、バージョニング・暗号化・アクセス制御の設定
- 実行環境の用意と、そこからGoogle Cloudへ認証する仕組み
- Terraformバージョンの固定と、その更新計画(固定しないと、実行環境によって挙動が変わります)
- 実行ログと、適用したリソースの一覧の保存
本記事の残りの内容が、その設計です。3番目を軽く見ないでください。Terraform本体とプロバイダのバージョンを固定していないと、誰も何も変えていないのにplanの結果が変わります。この現象は後半でもう一度扱います。
採用を検討する場合は、対応リソースの範囲と既存stateの移行可否を、事前に確認してください。
stateとリポジトリをどう分割するか
分割の話をモジュールで止めないでください。事故の範囲を決めるのは、モジュールの分割ではなくstateの分割です。
モジュール分割の基準は1つだけ
粒度を「小さく」「大きく」で語らないでください。基準は1つです。
同じライフサイクル、つまり変更のタイミングと頻度が同じリソースをまとめる。
VPCとサブネットは同時に変わります。しかしVPCとGKEのノードプールは、変わるタイミングがまったく違います。ノードプールは第4回のスケーリング設計に応じて何度も変わりますが、VPCは第3回で決めたら滅多に変わりません。これを同じモジュールに入れると、めったに変えたくないものを頻繁に触ることになります。
検証方法も1つです。「このモジュールを変更するとき、常に一緒に変わるリソースはどれか」と問う。答えが「一部だけ」なら、そこが分割の候補です。
state分割は3つの入力で決める
| 入力 | 見るもの | 分割の根拠になる状況 |
|---|---|---|
| 壊れたときの影響範囲 | 1回の誤ったapplyで何が壊れるか | 1つのstateに全環境が入っている。第3回で環境ごとにクラスタを分けたのと同じ論理 |
| applyの所要時間 | 1回のapplyにかかる時間 | 変更のリードタイムが伸びている。途中で失敗したときの状態が複雑になっている |
| 変更する主体 | 誰がそのコードを触るか | 第2回のRACIで担当が違うリソースが同じstateに入っている |
3番目を補足します。運用チームだけが触る部分と、他のチームも触る部分が同じstateにあると、権限を分けられません。stateの単位が、そのまま権限の単位になります。第2回で責任を分け、第5回で権限を分けたのと同じ線を、ここでも引いてください。
そして繰り返します。モジュールを分けてもstateが1つなら、事故の範囲は変わりません。モジュール分割は可読性と再利用性の話であり、影響範囲の話ではありません。
分割には2つの代償がある
分割を推奨してきましたが、代償も明示します。
1つ目、stateをまたぐ依存が発生します。ネットワークのstateで作ったサブネットのIDを、クラスタのstateから参照する必要が出ます。参照の方法はいくつかありますが、どの方法を採っても「先に適用しなければならない順序」が生まれます。分割した数だけ、適用の順序が制約になります。
この代償を小さくする方法は1つです。依存の向きを一方向に保つこと。ネットワーク → クラスタ → ワークロードのように、下位から上位への一方向にします。相互参照が生まれた時点で、その2つは分けるべきではなかったということです。
2つ目、分割の変更は後からやると高くつきます。stateを分けるということは、既存のリソースを別のstateへ移す作業を伴います。コードを書き直すだけでは終わりません。
したがって、分割の方針は最初に決めてください。「まず1つのstateで始めて、大きくなったら分ける」は、分けるコストを将来に先送りしているだけです。第1回で「後から変えられない設計項目」を意識したのと同じ扱いをしてください。max Pods per node がクラスタの作り直しを要求したのと同様、stateの分割方針も実質的に作り直しに近い作業になります。
環境分離の実装方式
前提を確認します。環境をどう分けるかという方針は、第3回で決定済みです。本記事はその実装方式だけを扱います。
判断基準はこうです。環境間で構成そのものが違うなら、ディレクトリ分割。Workspaceは、同じ構成をパラメータだけ変えて複製する用途に向いています。
第3回の決定を思い出してください。ProdとStgはリージョンクラスタ、Devはゾーンクラスタと決めました。この時点で、DevはProdと構成が違います。
一方で、第3回では「StgはProdのミラー環境」とも決めました。そしてミラーの定義は構成の同一性であって、規模の同一性ではないと確認しました。つまりProdとStgは同じ構成でパラメータだけが違い、Devだけが違う構成です。
この非対称性が実装方式に効きます。ProdとStgの間はパラメータの差で表現でき、Devとの間はディレクトリの差で表現する。3環境を機械的に同じ方式で扱おうとすると、どちらかに無理が出ます。
モノレポかマルチレポか
判断基準は1つです。リポジトリの単位は、レビューする人の単位に合わせる。
別々の人がレビューすべきコードを1つのリポジトリに置くと、レビュー要求の設定が複雑になり、結局「誰でもマージできる」状態に落ち着きます。逆に、同じ人が常に一緒にレビューするコードを分けると、関連する変更が2つのプルリクエストに分かれ、片方だけマージされる事故が起きます。
ここで境界を再確認します。Kubernetesマニフェストのリポジトリ構成は第10回です。インフラコードとマニフェストを同じリポジトリに置くかどうかは、両方の要件が揃う第10回で判断してください。本記事の時点で決めると、片方の要件だけで決めることになります。
planとapplyを分ける
この分離は、自動化の話ではありません。運開分離の実装そのものです。
権限の非対称性を設計に使う
planは読み取り権限で実行できます。applyには書き込み権限が要ります。この非対称性が、そのまま設計に使えます。
人間には planの権限だけを渡し、applyは承認を経たパイプラインだけが実行できるようにします。
これは第2回・第5回でやったことの繰り返しです。第2回で「マニフェストの記述」と「本番クラスタへの適用」を別のタスクに分けました。第5回でそれをKubernetes RBACに落とし、開発者に本番の書き込み権限を渡さない設計にしました。IaCでは、同じ分離をCloud IAMで実装します。主体が違うだけで、構造は同じです。
そしてこれは第9回で扱うGitOpsと同じ構造でもあります。人間が直接適用せず、パイプラインまたはコントローラが適用する。結果として、「誰が適用したか」ではなく「どのコミットが適用されたか」で追跡できる状態になります。監査で問われるのは後者です。

CIからサービスアカウントキーを排除する
第5回で、組織のポリシー制約によりサービスアカウントキーの作成を禁止すると決めました。その決定に従うなら、CIから鍵で認証する構成はそもそも作れません。
代わりに Workload Identity Federation を使います。第5回で扱ったのはGKE向けのものでしたが、汎用のWorkload Identity Federationは外部のIDプロバイダにも対応します。
- AWS・Azure などのクラウドプラットフォーム
- GitHub や GitLab といったデプロイサービス
- Kubernetes
- OIDC・SAML 2.0 に対応したプロバイダ
- オンプレミスのActive Directory、X.509クライアント証明書
公式の説明では、サービスアカウントキーは「正しく管理されなければセキュリティリスクになりうる強力な認証情報」であり、フェデレーションは「サービスアカウントキーに伴う保守とセキュリティの負担をなくす」とされています。
設計の順序を確認してください。第5回の決定が、第8回のCI/CDの構成を決めています。逆ではありません。CIの都合で鍵を使いたいという要求が出たら、それは第5回の設計に戻る話です。
planの結果をレビュー対象にする
planの出力をプルリクエストにコメントとして残し、レビュー対象にしてください。
レビューするのはコードの差分ではなく、planの差分です。コードを読んで影響を推測するより、planが「何を作り、何を変更し、何を破壊するか」を直接示します。特に、意図しない再作成(リソースの置き換え)は、コードの差分からは読み取れません。
ただし、前章で確認した制約を先に満たしてください。planファイルにはシークレットが含まれます。出力に機微な値が入らないことを実際の出力で確認してから、この構成を作ります。順序を逆にすると、確認する頃には過去のプルリクエストすべてに残っています。
2つの注意点があります。
1つ目、planとapplyの間に時間が空くと、planの前提が変わっている可能性があります。他のプルリクエストが先にマージされた、手動で何かが変更された、といった理由です。apply直前に再度planを実行し、承認時のものと一致するかを確認する設計にしてください。一致しなければ止めます。
2つ目、リソースの削除を含むplanは、別の承認経路にしてください。作成と変更は自動、削除は人間の明示的な承認、という分け方が現実的です。第7回でPVCの削除について「消す事故と消さない事故は方向が逆だ」と書きましたが、IaCでも同じで、削除だけは取り返しがつきません。
誰も変更していないのにplanに差分が出る状態を検出する
最後に、定期的なplanの実行を設計に入れてください。変更のたびではなく、変更がないときに実行します。
誰もコードを変更していないのにplanが差分を出すなら、実環境がコードから離れています。原因は3つのどれかです。
- 誰かがコンソールで手を入れた。障害対応の最中であることが多く、悪意はありません。しかし記録が残っていなければ、次の apply で元に戻されます
- クラウド側が自動で変更した。GKEのリリースチャンネルによる更新などです。第3回で「自動化されるのは上げる作業だけ」と確認したのと同じ構造で、自動更新される項目をコードで固定しようとすると、永久に差分が出続けます
- プロバイダのバージョンが上がって、既定値の扱いが変わった。コードも実環境も変わっていないのに差分が出ます
3つとも対処が違います。1番目はコードへの反映か切り戻し、2番目はその項目をコードの管理対象から外す、3番目はプロバイダのバージョンを固定します。
この検出がないと、差分は再現テストの当日まで見つかりません。そして再現テストは、本番を作り直そうとしている場面で行われることもあります。定期planは、再現テストの前倒しです。
【実務テンプレート】IaC・リポジトリ構成設計書
第2回から第7回の成果物が入力になります。
セクション1: 完了条件
完了条件: 空のプロジェクトから再現できること(applyが通ることではない)
再現テストの周期: ____ヶ月ごと / 次回____/____/____
前回の実施日と結果: ____/____/____ [成功 / 失敗]
再現テストを実施するトリガー: [クラスタのバージョン更新 / モジュール構造の変更 / 新しい種類のリソースの追加 / 手動リソースの取り込み]
セクション2: IaC管理外のリソース
リソースA: ______ → [取り込む(期限____/____/____) / 意図的に管理外]
管理外とする理由: ______________________________
リソースB: ______ → [取り込む(期限____/____/____) / 意図的に管理外]
管理外とする理由: ______________________________
棚卸しの周期: ____ヶ月
セクション3: stateの保護
バックエンド: [Infrastructure Manager / GCS / その他______]
Infrastructure Manager を却下した場合の理由(第6回の3条件のどれか): ______
GCSの場合
バケット名: ______ / オブジェクトのバージョニング: [有効 / 無効]
暗号化: [Cloud KMS(kms_encryption_key) / 顧客提供鍵(encryption_key) / なし]
顧客提供鍵の場合、鍵の紛失=データ喪失であることを承知しているか: [はい]
prefix の設計: ______________________________
監査ログ: [有効 / 無効]
stateバケットの読み取り権限を持つ主体(第5回のRBACマトリクスと同形式)
主体1: ______ / 権限______ / 必要な理由______
主体2: ______ / 権限______ / 必要な理由______
→ この一覧が、シークレットを読める人の一覧そのものになる
セクション4: Terraformに通さない値
Terraformで扱わないシークレットの一覧: ______________________________
それらをどう投入するか: ______________________________
Terraformが作るのは「入れ物」だけになっているか: [はい / いいえ(例外______)]
セクション5: モジュールとstateの分割
モジュール一覧と、それぞれがまとめている「同じライフサイクル」の説明
モジュールA: ______ / 一緒に変わる理由______
モジュールB: ______ / 一緒に変わる理由______
state一覧
state-1: ______ / 分割の根拠[影響範囲 / apply時間 / 変更主体] / apply所要時間____分
state-2: ______ / 分割の根拠[影響範囲 / apply時間 / 変更主体] / apply所要時間____分
→ 変更主体で分けた場合、第2回のRACIの担当と一致しているか: [はい / いいえ]
セクション6: 環境分離とリポジトリ
第3回で決めた環境ごとの構成: Prod[______] / Stg[______] / Dev[______]
構成が同一の組み合わせ: ______ → パラメータ差で表現
構成が異なる組み合わせ: ______ → ディレクトリ分割で表現
実装方式: [ディレクトリ分割 / Workspace / 併用]
リポジトリ構成: [モノレポ / マルチレポ] / レビュー担当の単位: ______
※ K8sマニフェストのリポジトリは第10回で判断する
セクション7: CI/CD
認証方式: [Workload Identity Federation / サービスアカウントキー]
鍵を使う場合、第5回の組織ポリシーと矛盾しないか: [確認済 / 未確認]
planの実行条件: ______________________________
planの結果をPRに出力しているか: [はい / いいえ]
出力する場合、planに機微な値が含まれないことを実際の出力で確認したか: [確認済 / 未確認]
applyの承認経路: ______________________________
人間に apply の権限を渡していないか: [確認済 / 未確認]
apply直前の再plan: [実施する / しない] / 承認時と不一致の場合の挙動: [中止 / 続行]
削除を含むplanの承認経路: ______________________________(作成・変更とは別経路にする)
ドリフト検出
変更がないときの定期plan: [実施する / しない] / 周期____
差分が出た場合の分類と対処
[手動変更] → コードへ反映 / 切り戻し
[クラウド側の自動変更] → その項目をコードの管理対象から外す
[プロバイダのバージョン変更] → バージョンを固定する
Terraform本体とプロバイダのバージョン固定: [固定済 / 未固定] / 更新の周期____
次回予告
インフラのコードが管理できるようになりました。次に決めるのは、その上に載るアプリケーションをどう届けるかです。
次回の第9回「CI/CDパイプライン・サプライチェーン設計書」では、イメージのビルド、脆弱性スキャンのパイプラインへの組み込み、イメージ署名、コンテナレジストリの設計、そして環境間のイメージ昇格フローと承認ルールを扱います。
本記事で「planとapplyを分ける」という形で実装した運開分離が、第9回では「ビルドとデプロイを分ける」という形で現れます。そして第7回で「コンテナイメージはバックアップされない」と確認した問題は、第9回のレジストリのライフサイクル設計で手当てします。バックアップの保持世代と、イメージの保持期間。この2つを突き合わせるのが第9回の仕事の1つです。
