Terraformで複数の実行環境(dev・stg・prodなど)を運用するとき、環境差分をどの手段で吸収しbackend configをどう分けるかという設計判断は、チームの保守コストと本番適用ミスのリスクを直接左右します。tfvars・workspace・ディレクトリ分割はいずれも有効な手段ですが、選択の根拠が曖昧なまま運用が始まると「環境を増やすたびにコピペが増える」「シークレットがstateに混入する」「本番の変数が想定と違っていても気づけない」という問題が積み重なります。本記事では、差分の性質に応じた方式の選択軸から、backend config設計・シークレット分離・レビュー承認フロー・新環境追加時の漏れ防止まで、設計判断の根拠を体系化します。
環境差分をどこで吸収するか(tfvars・locals・workspace・別ディレクトリ)の選択軸
差分の性質による選択肢の絞り込み
環境間の差分は「値の差分」「構成の差分」「スコープの差分」の3種類に分けられます。値の差分とは、インスタンスタイプやレプリカ数など同じリソース構成でパラメータだけ異なるケースで、tfvarsで吸収できます。構成の差分とは、stagingにはなくprodにだけ存在するリソース(WAF・バックアップスケジュールなど)を指し、count/for_eachの条件値として変数を渡すか、ディレクトリを分割するかの判断が必要です。スコープの差分とは、AWSアカウントやVPCが環境ごとに完全に異なるケースで、この規模ではworkspaceよりディレクトリ分割が安全です。まず自分たちの差分がどの種類に属するかを特定することが、方式選定の出発点になります。
localsとvariableの役割分担を明確にする
variableは「外部から注入する値」、localsはその変数を加工して内部で使う中間値、という役割分担が崩れると設計が複雑になります。たとえば、環境名をプレフィックスに付けたリソース名はlocalsが適切ですが、localsに環境固有の値をハードコードすることは避けます。この混在が続くと、tfvarsには書かれていない差分がコード中に散在し、変数管理の全体像が把握しにくくなります。設計方針として「外部から変えたい値は必ずvariableに定義し、localsはその変換ロジックのみを担う」と明文化することで、後からコードを読む人が差分の所在を追いやすくなります。
選択を誤ると何が起きるか
workspaceをディレクトリ分割の代替として使うと、prodだけ微妙にリソース構成が異なるケースで terraform.workspace による条件分岐がコード内に増えます。この分岐が5箇所を超えると、どの環境でどの分岐が通るかの把握が難しくなり、レビューでの見落としリスクが上がります。逆にディレクトリを細かく分けすぎると、共通モジュールへの変更を複数のディレクトリで個別にapplyする運用が必要になり、手順ミスの温床となります。「環境間の構成差分の大きさ」を主軸に、差分が値だけならtfvars集約、構成そのものが異なるならディレクトリ分割、という判断軸を設計段階でチームが合意しておくことが重要です。
tfvars方式・workspace方式・ディレクトリ分割方式の比較表
3方式の特性比較
以下の比較表は、方式選定の主要な観点をまとめたものです。「環境間の構成差分の大きさ」と「CI/CDパイプラインの成熟度」を軸に選定してください。
| 観点 | tfvars方式 | workspace方式 | ディレクトリ分割方式 |
|---|---|---|---|
| 環境間の構成差分 | 小〜中(値の差のみ) | 小(条件分岐でコード内に表現) | 中〜大(構成そのものが異なる) |
| backend管理 | 環境ごとに個別設定しやすい | workspace名でstate自動分割(同一bucket内) | ディレクトリごとに独立して定義 |
| 新環境追加コスト | 低(tfvarsを1ファイル追加) | 低(workspace newのみ) | 高(ディレクトリ一式を複製・調整) |
| コードの見通し | 良(コードはenv非依存) | 悪(条件分岐が増えると読みにくい) | 良(各ディレクトリが独立) |
| 誤適用リスク | 低(明示的にファイルを指定) | 中(workspace切り替え忘れによる誤操作) | 低(物理的に分離) |
| 主な用途 | dev/stg/prod 3環境・構成が同一 | 短命な機能ブランチ検証環境 | マルチアカウント・大規模組織 |
方式選定のチェックリスト
以下のフローで方式を絞り込むと判断が整理されます。上から順に評価し、最初に「はい」となった条件に対応する方式を優先してください。
- 環境ごとにAWSアカウントが異なるか? → はいならディレクトリ分割を選択する
- prodとstgでリソースの有無が異なるか(WAF・バックアップ等)? → はいならディレクトリ分割、またはtfvars+countの組み合わせを評価する
- 差分が値(サイズ・台数)だけか? → tfvars方式で十分
- 短命な検証環境を頻繁に作成・削除するか? → workspace方式が有効
- CIでworkspaceの切り替えミスを防ぐ仕組みがあるか? → なければworkspace採用を再検討する
workspaceを長期運用に使う場合の注意点
workspaceは短命な環境には有効ですが、prodと同等の長期環境に適用すると問題が起きやすいです。全workspaceで同一のstateバックエンド設定を共有する構造上、backend configの差異(暗号化キー・ロック設定など)を環境ごとに変えられません。また、workspace切り替え忘れによる誤apply事故は運用現場でよく発生するリスクです。長期運用が前提のprod相当環境には、tfvars方式かディレクトリ分割方式を選択することを推奨します。workspaceはあくまでも使い捨て検証環境のライフサイクル管理に限定する、という設計原則を明文化しておくと運用上の混乱を防げます。
backend configの環境別分割と変数注入の設計
partial configurationパターンの採用判断
Terraformのbackendブロックは通常の変数(variable)を受け取れません。この制約への対処として広く使われるのがpartial configuration(部分設定)パターンです。backendブロックにbackendの種別のみを記述し、残りの設定を -backend-config フラグでファイルまたはKV形式で注入します。環境ごとに `backend.dev.hcl`・`backend.prod.hcl` を用意しておくことで、同一のmain.tfをすべての環境で使い回せます。backend configファイルはTerraformコードと同一リポジトリに配置するか別リポジトリで管理するかを設計段階で決め、置き場所をチームで統一します。
backend configファイルの設計方針
backend configファイルに含める最低限の項目は、stateファイルのbucket名・key(パス)・region・DynamoDBテーブル名(ロック用)です。keyのパス設計は後から変更するとstate移行作業が発生するため、設計段階で確定させます。複数プロジェクトが同一バケットを共有する場合は `projects/
CIパイプラインでのbackend config注入
CI環境では、ジョブ変数やシークレットストアからbackend configの値を注入する方法が一般的です。パブリックリポジトリの場合、バケット名やリージョン情報をbackend configファイルに含めることはセキュリティ上避けるべきで、CIのセキュア変数から実行時に動的に生成するフローにします。プライベートリポジトリであれば、backend configファイルをリポジトリに含めてコードレビュー対象にすることで変更履歴が残り監査にも役立ちます。どちらの方針にするかは、組織のセキュリティポリシーとリポジトリの公開範囲を基準に選択します。
シークレットと非シークレット変数の分離管理設計
変数ファイルの分割粒度
tfvarsファイルを1環境1ファイルにまとめると、DBパスワード等のシークレットとインスタンスタイプ等の非シークレットが同一ファイルに混在します。この状態でリポジトリに含めるとシークレットが露出するため分割が必要です。設計方針として、`prod.tfvars`(非シークレット・リポジトリ管理)と `prod.secrets.auto.tfvars`(シークレット・gitignore対象)に分割します。後者は実行時にVault・AWS Secrets Manager・CIシークレットから動的に生成し、plan/apply後に削除するフローにします。このファイルの生成・削除ステップをCIに組み込み、ローカル実行時の手順もドキュメントとして明文化します。
Vault・Secrets Managerとの連携判断
HashiCorp VaultやAWS Secrets Managerをシークレット供給源にする方法は2つあります。TerraformのVaultプロバイダーやdataソースで直接取得する方法は、取得ロジックがコードに明示される反面、Vaultへの認証設定がTerraformの実行コンテキストに必要です。CIパイプラインで事前に取得して環境変数に変換する方法はシークレット取得ロジックをCIに隔離できますが、CIスクリプトの管理コストが上がります。既存のCIスタックや組織のシークレット管理ツールとの整合性を優先して判断し、設計根拠を記録しておきます。
tfstateへのシークレット混入リスクへの対処
変数でシークレットを渡したとしても、そのリソース(RDSのpasswordなど)はtfstateに平文で記録されます。これはTerraformの仕様上避けられないため、stateバックエンドのS3バケットにはSSEとアクセス制限を設定し、stateへのアクセス権限をインフラチームに限定します。variable定義に sensitive = true を付けるとplan/applyの出力にシークレット値が表示されなくなりますが、state内データを暗号化するものではありません。この違いを設計ドキュメントに明記し、シークレット保護の責任がstateバックエンドのアクセス制御にあることをチームで共有します。
変数定義ファイルのレビュー承認フロー設計
tfvarsをPull Requestに含めるかどうかの判断
シークレットを分離した非シークレットのtfvarsファイルはリポジトリに含めてPull Request管理することを基本方針にします。これにより、環境パラメータの変更がコードレビューのプロセスに乗り、変更履歴がgitログに残ります。よくあるつまずきとして「インフラパラメータの変更はコードと分けて管理したい」という理由でtfvarsをリポジトリ外で管理するケースがあります。この場合、変更履歴が失われ誰がいつパラメータを変えたかを追跡できなくなります。環境パラメータの変更も「コードの変更と同等の変更管理」として扱うことを設計原則として明文化します。
本番環境変数の変更承認ゲート設計
prodのtfvarsファイルへの変更には、devやstgより厳格なレビュープロセスを設定します。GitHubやGitLabのCODEOWNERS機能を使い、`environments/prod/*.tfvars` へのPull Requestに対してインフラリードの承認を必須にします。さらにprod環境へのapplyは、plan結果の確認コメントをPull Requestに残すことをルール化し、AtlantisなどのツールでPull Requestにplan/applyを統合する構成が効果的です。承認ゲートにより、インスタンスタイプの変更やレプリカ数の増減が意図せず適用されるリスクを設計として抑制できます。
plan差分とtfvars変更の紐づけ確認
tfvarsの変更内容がterraform planの差分として正しく反映されているかを確認するステップをレビューフローに組み込みます。Pull Requestにterraform planの出力をコメントとして添付する運用では、「tfvarsの変更内容→planの差分→期待する変更の一致」を確認する項目をPull Requestテンプレートに含めます。想定例として、5つの環境を管理するチームでは、このフローにより変数の見落としをapply前に発見できる確率が上がります。変数の変更が意図した環境にのみ影響することをplanで確認し、意図しない環境への波及を防ぐことがこのステップの目的です。
デフォルト値と上書きのルールを共有するためのREADME設計
README設計の4要素
variables.tfに定義された変数のうち、デフォルト値を持つものと必須(デフォルトなし)のものが混在すると、どの変数を各tfvarsで設定しなければならないかがコードを読まないとわからなくなります。モジュールや環境ディレクトリのREADMEには、「必須変数の一覧」「デフォルト値の一覧とその意図」「環境ごとの上書きルール」「上書きが非推奨または禁止の変数」の4要素を記載します。特に「上書きが非推奨の変数」はセキュリティや設計上の理由で固定したいパラメータを明示するもので、設計意思決定を文書として残す効果があります。この4要素をREADMEのテンプレート化しておくと、新しいモジュールを追加するたびに記載が統一されます。
デフォルト値の「合意」を文書化する
variables.tfに `default = “t3.micro”` と書いても、なぜその値がデフォルトかという根拠はコードに残りません。READMEにその意図(「開発コスト削減のためdevではt3.microをデフォルトとし、stgとprodはtfvarsで必ず上書きする」など)を1〜2文で記載します。この記述がないと、後からチームに加わったメンバーがデフォルト値を本番でもそのまま使うリスクがあります。デフォルト値の合意は設計判断の記録です。コード上で追いにくいものほどREADMEに根拠を残す原則を徹底することで、設計意図が時間をかけて薄れていくことを防ぎます。
型・制約をvariables.tfに集約する
変数の型定義とvalidationブロックをvariables.tfに集約すると、不正な値が渡された場合のエラーをplan時点で検出できます。たとえば `environment` 変数に `validation { condition = contains([“dev”,”stg”,”prod”], var.environment) }` を設定すると、typoによる誤った環境名でのapplyを防げます。このような制約はREADMEに記載するだけでなく、variables.tfのvalidationとして実装することで「仕様を実行可能な形で表現」できます。変数の定義・制約・デフォルト・README解説を一体として管理する構造を設計方針として定めると、チームの保守コストが下がります。
新環境追加時の変数コピー漏れを防ぐ設計
テンプレートtfvarsの設計
新環境追加時に最も起きやすい問題が、既存環境のtfvarsをコピーする際の変数設定漏れです。対策として、variables.tfで定義されたすべての変数キーを網羅した `_template.tfvars`(値は「FIXME:」プレースホルダーか型の例示)をリポジトリに配置します。新環境追加時はこのテンプレートをコピーしてすべてのキーに値を設定するプロセスにすることで、キーの欠落が起きにくくなります。テンプレートのキー一覧はvariables.tfの定義から生成するスクリプトで自動更新し、変数の追加・削除に追従させます。スクリプトはCIのlintステップに組み込み、テンプレートの乖離を自動検出する設計にします。
CIによる自動検出の設計
variables.tfで定義されたすべての変数キーが、各環境のtfvarsまたはdefault値でカバーされているかを検証するCIチェックを設けます。このチェックを毎回のPull Requestで実行することで、変数を追加したコード変更が一部の環境のtfvarsに反映されないままマージされることを防ぎます。よくあるつまずきとして「CI検証スクリプトが変数のネスト(object型)を正しく展開できない」ケースがあります。検証スクリプトはシンプルなスカラー変数から実装を始め、後から複合型に対応する段階的な設計を推奨します。手を動かす実装の詳細はセミナーや本体リソースで補完してください。
変数スコープの棚卸しサイクルの設計
変数は追加されることが多い一方、使われなくなった変数がtfvarsに残り続けるケースもよくあります。3〜6か月に1回程度の棚卸しサイクルを設け、「variables.tfに定義があるがどのtfvarsにも設定がない変数」と「tfvarsに記載があるがvariables.tfに定義がない変数」を洗い出します。自動化で検出できますが、変数を削除するかどうかの判断は将来のフィーチャーフラグや特定環境限定の変数といった背景を踏まえた人による確認が必要です。棚卸しの判断理由をPull RequestやIssueに記録することで、次回の棚卸し時の参照にでき、設計意思決定の蓄積につながります。
▶ 手を動かして本番まで到達する
設計の型は掴めても、本番でTerraformを通しきる経験は別物です
ここまでの設計判断は、読めば腹に落ちます。ですが「自分の手で本番まで通しきる自信」は、それとは別の話になります。
2日間の集中ハンズオンで、AWS上にIaCで構築しきる経験を持ち帰れます。設計判断を、動くコードと本番環境の手応えに変える場です。
Terraform複数環境の変数管理設計を実践するTerraformコースへ →
Terraformマスターセミナー|2日間ハンズオンでAWS上にIaCで本番構築