「環境ごとにコードが違う」状態は、いずれprodだけ動かない事故を生みます。同じコードを差分だけ変えて前へ進める昇格の設計を、変数分離・状態分離・承認・ロールバックまで一気通貫で整理します。

「stgでは通ったのにprodだけ落ちる」。この事故のほとんどは、環境ごとにコードを分けて育ててしまった結果です。本稿では、同じコードをdevstgprodへ昇格(promote)させるという発想を軸に、変数の分離、状態(state)の分離、prod適用の承認、ロールバック、環境ドリフトの防止までを、実務の粒度で整理します。Terraform/OpenTofuを前提に書きますが、考え方はどのIaCでも同じです。

01昇格とは「同じコードを進める」こと

環境昇格の基本原則は一つです。環境間で変えてよいのは「値」だけで、「コード」は変えない。dev用のコードとprod用のコードが枝分かれした瞬間、stgでの検証はprodの保証になりません。理想は、devで動いたのと寸分違わぬコードを、変数だけ差し替えてstg、prodへ順に流していく形です。

HashiCorpのworkspace解説でも、環境をworkspaceで分ける狙いは「環境同士をできる限り似せたまま、同じコードで異なるstateを持つ」ことだと明記されています(Terraform: State Workspaces)。似せることそのものが目的、という視点が出発点です。

現場のコツ:「prodだけ特別扱い」の要望は必ず出ます。そのとき、コードを分岐させるのではなく、変数(インスタンスサイズ、冗長度、フラグ)で吸収できないかをまず疑ってください。コード分岐は最後の手段です。

02差分は変数/tfvarsに追い出す

環境差は.tfvarsファイルに閉じ込めます。コード(.tf)は共通、値は環境ごとのファイルという分担です。

ここで効くのが変数のバリデーションです。variableブロックのvalidationで「prodではインスタンスタイプにt系を許さない」といった制約をコード側に持たせておくと、tfvarsの取り違えを適用前に弾けます。値を外に出しつつ、危険な値の範囲はコードで守る、という二段構えです。

共通コード (.tf) 1つだけ。分岐させない dev.tfvars stg.tfvars prod.tfvars dev 環境 stg 環境 prod 環境 昇格 = 同じコード + 環境別の値で順に前へ
図:共通コードに環境別tfvarsを組み合わせ、devからprodへ同じコードを昇格させる。

02状態(state)の分離 — workspace vs ディレクトリ

コードを共通化しても、stateは環境ごとに完全に分けます。ここで方式選択が発生します。

選択の決め手は認証情報とアクセス制御を分けたいかです。HashiCorpの公式ドキュメントは、CLI workspaceについて「システム分解や、個別の認証情報・アクセス制御を要する構成には適さない(Workspaces are not appropriate for system decomposition or deployments requiring separate credentials and access controls.)」と明言しています(Terraform: State Workspaces)。理由は、CLI workspace群は同一backendを共有するため、prodとdevで別々のクレデンシャルやアクセス境界を張れないからです。

現場のコツ:エンタープライズでは「dev/stg/prodをアカウント分離する」のが定石です。この時点でbackendもクレデンシャルも別。したがってディレクトリ分割方式が素直な選択になります。CLI workspaceは、同一アカウント内のごく近い環境(例:PR単位の一時環境)に留めるのが安全です。アカウント分離の考え方はマルチアカウント統制も参照してください。

03ディレクトリ分割の重複をモジュールで吸収する

ディレクトリ分割の弱点は、各環境ディレクトリにコードが重複しがちなことです。ここを埋めるのがルートモジュール薄化のパターンです。

これにより「コードは共有モジュール1本、環境ディレクトリは配線とパラメータだけ」という状態になり、ディレクトリ分割でありながらコード実体の重複を避けられます。stateとクレデンシャルは環境ごとに独立、コードは共通、という理想形です。モジュール設計やstateの置き場所そのものはIaCの状態管理で掘り下げます。

04昇格を止めるゲート — prod適用の承認

昇格の途中には必ず人間の承認を挟みます。ここはCI/CDプラットフォームの機能に寄せるのが堅実です。

GitHub Actionsではenvironmentに対して保護ルールを設定でき、公式ドキュメントは「環境を参照するジョブは、実行やシークレットへのアクセスの前に、環境の保護ルールに従わねばならない」と述べています(GitHub Docs: Managing environments for deployment)。使える統制は主に次の3つです。

さらに、承認の実効性を担保するためにセルフレビュー禁止を有効化できます。デプロイを起こした本人が自分で承認することを禁じる設定で、prodは必ず二人以上の目を通す運用になります(前掲GitHub Docs)。管理者バイパスの不許可(disallow bypassing)も併せて設定すれば、抜け道を塞げます。

GitLabでも同じ思想が用意されており、Protected Environmentsで特定ユーザー/グループの承認をデプロイ前に必須化できます(GitLab Docs: Environments)。プラットフォームは違っても「prodゲートは承認・待機・ブランチ制限の三点セット」という設計は共通です。

現場のコツ:承認ゲートに回すのは「apply」ではなく「planの結果」です。terraform planの出力(差分)を成果物として承認画面に添付し、レビュアーが「このprod差分でよいか」を見て承認する。planを見ずにapplyだけ承認する運用は、事実上の白紙委任です。CI/CDパイプラインの組み方はIaCのCI/CDパイプラインで扱います。

stg 適用済 検証OK prod plan 差分を生成 承認ゲート 必須レビュアー セルフ承認禁止 prod apply 昇格完了 planの差分を承認画面に添付し、人がprod差分を確認してから初めてapplyへ進む
図:prodはplanの差分を承認ゲートに通してからapplyする。承認対象はapplyではなくplan結果。

05ロールバック — 「戻す」より「進めて直す」

インフラのロールバックは、アプリのそれより難しい問題です。前提を分けて考えます。

  • コードは戻せる:Gitで前のコミットに戻し、そのコードで再度applyすれば、宣言的定義としては旧状態を再現できる。これが基本のロールバックです。
  • データは戻らない:削除したリソースやマイグレーション済みのデータは、コードを戻しても復元されません。破壊的変更は昇格前に別扱いにする必要があります。

実務では「戻す(revert)」より「前へ進めて直す(roll forward)」が現実的な場面が多いです。ただし、GitLabのようにデプロイ履歴から過去のデプロイを選んでロールバックする機能を持つプラットフォームもあり、環境ごとのデプロイ履歴が commit に紐づいて記録されます(GitLab Docs: Deployments)。どの版が今prodに乗っているかを常に追跡できる状態を保つことが、ロールバック可否の土台になります。

現場のコツ:DB削除・スナップショット削除・prevent_destroy対象など「戻せない変更」は、昇格パイプラインとは別の、より重い承認経路に隔離してください。すべてを同じ昇格フローに乗せると、ワンクリックで不可逆な事故が起きます。

06環境ドリフトの防止 — 昇格を無意味にしないために

せっかく同じコードを昇格させても、prodで誰かがコンソールから手作業で設定を変えれば、コードと現実がずれます。これが環境ドリフトで、放置すると次のapplyで意図しない差分が現れ、昇格の前提が崩れます。

Terraformではterraform plan -refresh-onlyで、stateファイルと実インフラの乖離を検出できます。公式ドキュメントも「refresh-onlyモードは、stateファイルへの変更提案をレビューできるようにすることで、実インフラとの照合を安全にする」と説明しています(Terraform: Use refresh-only mode to sync Terraform state)。運用としては次を組み合わせます。

  • 各環境で-refresh-onlyのplanを定期実行(日次など)し、差分が出たら通知する。
  • ドリフトを検知したら、原則はコードを正とし、コードに寄せる形で解消する。手変更を追認する場合はコードへ反映する。
  • そもそも手変更を許さないよう、prodのコンソール書き込み権限を絞る。最小権限の実像はマルチアカウント統制の考え方が効きます。

ドリフト検知の設計・運用は単独で深掘りする価値があるため、詳細はIaCのドリフト検知に譲ります。ここでは「昇格の健全性はドリフト防止とセットで初めて成立する」ことを押さえてください。

07昇格パイプライン設計のチェックリスト

ここまでを、着手時の点検項目としてまとめます。

  • 環境差は.tfvarsに閉じ込め、.tfは環境間で分岐させていないか。
  • 危険な値(prodでの過小スペック等)はvalidationでコード側にガードを置いたか。
  • state・backend・クレデンシャルは環境ごとに分離したか(基本はディレクトリ分割)。
  • 共有モジュールをバージョン固定し、環境ごとに段階昇格しているか。
  • prodはplan結果を承認対象にし、必須レビュアー+セルフ承認禁止を掛けたか。
  • 不可逆な破壊的変更を、通常の昇格フローから隔離したか。
  • -refresh-onlyの定期実行でドリフトを監視し、コードを正として戻す運用があるか。

この7点が埋まっていれば、「stgでは通ったのにprodだけ落ちる」事故の大半は構造的に防げます。逆に一つでも欠けると、そこが将来の障害点になります。ブランチ戦略との噛み合わせはGitHub Flow / GitLab Flow / トランクベースも併せてご覧ください。

まとめ

環境昇格の要諦は、「コードは一つ、値だけ変える」を徹底し、その上でstateとクレデンシャルを環境ごとに分離し、prodには承認ゲートを、全環境にはドリフト監視を掛けることです。派手な仕組みは要りません。同じコードが淡々と前へ進み、prodの手前で人が差分を確認し、現実がコードから逸れたら気づける。この地味な一気通貫こそが、エンタープライズのインフラ変更を止めずに、かつ安全に前へ進める基盤になります。現状のtfvars構成や承認フローの点検が必要でしたら、お問い合わせください。

参考(一次情報)

環境昇格パイプラインの設計・レビューはお問い合わせください。現状のtfvars構成と承認フローを拝見し、勘所をお伝えします。

相談する
← ブログ一覧へ戻る