「環境ごとにコードが違う」状態は、いずれprodだけ動かない事故を生みます。同じコードを差分だけ変えて前へ進める昇格の設計を、変数分離・状態分離・承認・ロールバックまで一気通貫で整理します。
「stgでは通ったのにprodだけ落ちる」。この事故のほとんどは、環境ごとにコードを分けて育ててしまった結果です。本稿では、同じコードをdev→stg→prodへ昇格(promote)させるという発想を軸に、変数の分離、状態(state)の分離、prod適用の承認、ロールバック、環境ドリフトの防止までを、実務の粒度で整理します。Terraform/OpenTofuを前提に書きますが、考え方はどのIaCでも同じです。
01昇格とは「同じコードを進める」こと
環境昇格の基本原則は一つです。環境間で変えてよいのは「値」だけで、「コード」は変えない。dev用のコードとprod用のコードが枝分かれした瞬間、stgでの検証はprodの保証になりません。理想は、devで動いたのと寸分違わぬコードを、変数だけ差し替えてstg、prodへ順に流していく形です。
HashiCorpのworkspace解説でも、環境をworkspaceで分ける狙いは「環境同士をできる限り似せたまま、同じコードで異なるstateを持つ」ことだと明記されています(Terraform: State Workspaces)。似せることそのものが目的、という視点が出発点です。
02差分は変数/tfvarsに追い出す
環境差は.tfvarsファイルに閉じ込めます。コード(.tf)は共通、値は環境ごとのファイルという分担です。
terraform apply -var-file=envs/dev.tfvarsのように、環境名のtfvarsを明示的に指定する。- tfvarsに書くのは、インスタンスタイプ、Multi-AZの有無、スケール数、ログ保持日数、コスト系のフラグなど「環境で変わってよい値」だけ。
- 環境をまたいで同じであるべき値(命名規則、タグ規約、モジュールのバージョン)はtfvarsに置かず、コードやlocalsで固定する。
ここで効くのが変数のバリデーションです。variableブロックのvalidationで「prodではインスタンスタイプにt系を許さない」といった制約をコード側に持たせておくと、tfvarsの取り違えを適用前に弾けます。値を外に出しつつ、危険な値の範囲はコードで守る、という二段構えです。
02状態(state)の分離 — workspace vs ディレクトリ
コードを共通化しても、stateは環境ごとに完全に分けます。ここで方式選択が発生します。
- CLI workspace方式:1つの作業ディレクトリの中に複数のstateを持ち、
terraform workspace select prodで切り替える。コードの重複はゼロ。 - ディレクトリ分割方式:
envs/dev/envs/prod/のように環境ごとにディレクトリ(とbackend)を分ける。
選択の決め手は認証情報とアクセス制御を分けたいかです。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で別々のクレデンシャルやアクセス境界を張れないからです。
03ディレクトリ分割の重複をモジュールで吸収する
ディレクトリ分割の弱点は、各環境ディレクトリにコードが重複しがちなことです。ここを埋めるのがルートモジュール薄化のパターンです。
- 実体のリソース定義は共有モジュール(
modules/)に集約する。 - 各環境ディレクトリのルートは、そのモジュールを
sourceで呼び、tfvarsとbackend設定を渡すだけの薄い層にする。 - モジュールは必ずバージョン固定(Gitタグやレジストリの
version)し、環境ごとに参照バージョンを段階的に上げていく。
これにより「コードは共有モジュール1本、環境ディレクトリは配線とパラメータだけ」という状態になり、ディレクトリ分割でありながらコード実体の重複を避けられます。stateとクレデンシャルは環境ごとに独立、コードは共通、という理想形です。モジュール設計やstateの置き場所そのものはIaCの状態管理で掘り下げます。
04昇格を止めるゲート — prod適用の承認
昇格の途中には必ず人間の承認を挟みます。ここはCI/CDプラットフォームの機能に寄せるのが堅実です。
GitHub Actionsではenvironmentに対して保護ルールを設定でき、公式ドキュメントは「環境を参照するジョブは、実行やシークレットへのアクセスの前に、環境の保護ルールに従わねばならない」と述べています(GitHub Docs: Managing environments for deployment)。使える統制は主に次の3つです。
- Required reviewers(必須レビュアー):指定した人/チームの承認がないとジョブが進まない。承認前は環境シークレットにも到達できない。
- Wait timer(待機タイマー):一定時間の強制待機を挟み、慌てた即時適用を防ぐ。
- Deployment branch/tag制限:prod環境へ流せるブランチ/タグを限定する。
さらに、承認の実効性を担保するためにセルフレビュー禁止を有効化できます。デプロイを起こした本人が自分で承認することを禁じる設定で、prodは必ず二人以上の目を通す運用になります(前掲GitHub Docs)。管理者バイパスの不許可(disallow bypassing)も併せて設定すれば、抜け道を塞げます。
GitLabでも同じ思想が用意されており、Protected Environmentsで特定ユーザー/グループの承認をデプロイ前に必須化できます(GitLab Docs: Environments)。プラットフォームは違っても「prodゲートは承認・待機・ブランチ制限の三点セット」という設計は共通です。
terraform planの出力(差分)を成果物として承認画面に添付し、レビュアーが「このprod差分でよいか」を見て承認する。planを見ずにapplyだけ承認する運用は、事実上の白紙委任です。CI/CDパイプラインの組み方はIaCのCI/CDパイプラインで扱います。
05ロールバック — 「戻す」より「進めて直す」
インフラのロールバックは、アプリのそれより難しい問題です。前提を分けて考えます。
- コードは戻せる:Gitで前のコミットに戻し、そのコードで再度applyすれば、宣言的定義としては旧状態を再現できる。これが基本のロールバックです。
- データは戻らない:削除したリソースやマイグレーション済みのデータは、コードを戻しても復元されません。破壊的変更は昇格前に別扱いにする必要があります。
実務では「戻す(revert)」より「前へ進めて直す(roll forward)」が現実的な場面が多いです。ただし、GitLabのようにデプロイ履歴から過去のデプロイを選んでロールバックする機能を持つプラットフォームもあり、環境ごとのデプロイ履歴が commit に紐づいて記録されます(GitLab Docs: Deployments)。どの版が今prodに乗っているかを常に追跡できる状態を保つことが、ロールバック可否の土台になります。
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構成や承認フローの点検が必要でしたら、お問い合わせください。
—参考(一次情報)
- Terraform: State — Workspaces(HashiCorp Developer)
- Managing environments for deployment(GitHub Docs)
- Environments(GitLab Docs)
- Deployments(GitLab Docs)
- Use refresh-only mode to sync Terraform state(HashiCorp Developer)