2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

複数環境管理におけるTerraform Workspace利用の是非を、改めて考えてみる

Last updated at Posted at 2025-12-13

目次


1. はじめに

想定対象読者:

この記事は以下のような方を対象としています:

  • Terraformでのdev/stg/prdなどの複数環境管理方法について検討している方
  • Terraform Workspaceでの実際の運用経験に基づいた情報を求めている方

注意事項:

  • HCP Terraform(Terraform Cloud)のWorkspaceではなく、Terraform CLIのWorkspace機能に関する内容です。
  • 文中ではTerraformでAWSを利用することを前提としています。(おそらく他クラウドでも応用可能な内容です。)

前提知識:

以下の知識を理解していることを前提としています:

  • Terraformの基本的な使い方
  • Terraform Workspaceの基本的な概念
  • ディレクトリで複数環境管理するプラクティス

記事の経緯

最近、以下の記事をチーム内で共有し、Terraformのリポジトリ構成について議論をする機会がありました。

自分の観測範囲だと、Terraform Workspaceの機能を用いた永続的な複数環境作成は非推奨であり、ディレクトリレベルで環境を切り分け、モジュールを利用する方法をベストプラクティスとして推奨するという意見が一般的でありました。

しかしながら、上記記事を踏まえるとTerraform Workspaceやっぱり結構良いんじゃない?という気がしてきた為、改めてTerraform Workspaceを用いた複数環境管理の是非について整理して再評価してみようと思いました。

前提となる「なぜインフラコードのモジュール化は難しいのか」について、
詳細は元記事を参照いただきたいのですが、本記事に密接に関連する内容としてはざっくり以下のようなものです。

  • インフラコードは状態を管理するため、関心事が詳細になる
  • そのため、抽象化のためのモジュール化はすべきではなく、同一構成のパターンの再利用のために使用するべき

その前提のうえで、モジュール設計の原則として以下のようなポイントが挙げられています。

  • 無理にモジュール化しない(YAGNI)
  • 機能凝集を意識する
  • 階層を深くしすぎない、階層の深さを揃える
  • 組織や機能に適したデフォルト設定を内部で定義する
  • パブリックモジュールは使わず、組織内で管理する

この内容についてはそのとおりだと思いますので、
上記を踏まえたうえで、ディレクトリ分割とWorkspaceの2つの環境分割アプローチを比較し、
それぞれのメリット・デメリット、workspaceのよくある懸念点とその対処方法を実際の運用経験を交えて紹介していきます。


2. 環境分割の2つのアプローチ

2.1 ディレクトリでの環境分割

※あくまで一般的な例示です。実際の構成はプロジェクトによって異なります。

environments/
├── dev/
        component_a/
        component_b/
├── stg/
        component_a/
        component_b/
└── prd/
        component_a/
        component_b/
modules/
├── xx/
└── yy/
  • 環境ごとの異なる要件に対応しやすい
  • 環境ごとに完全に分離された状態管理ができる
  • 環境間で共通のものはmodule化して再利用できる

2.2 ディレクトリ分割の懸念点

(あくまで個人的な感覚値です。)

1. モジュール設計が難しい

基本的に各環境にそのままresouceを定義する形だと管理が煩雑になるため、
環境間の構成の共通化を図ろうとすると、ほぼ確実にモジュール化が必要になります。

moduleについては機能凝集を意識する必要がありますが、
ディレクトリ分割型の場合、環境での再利用という観点がほぼ確実にモジュール設計に影響を与えます。

  • モジュールを小さい単位に分割しすぎると、各環境ディレクトリでのモジュール呼び出しが煩雑になる
  • モジュールを大きい単位にまとめすぎると、環境ごとの微妙な差分への対応によってモジュールへの変数追加が増え、どうしても複雑化する
  • そもそも環境での再利用という目的のためにモジュールを使用してしまっている
  • その結果として、同一環境における共通パターンでの再利用 という目的でモジュールを扱いづらい

もちろん、適切なモジュール設計ができれば、上記の問題は軽減されるのでしょうが、
それなりの規模のプロジェクトで複数環境を管理する場合、モジュール設計の難易度が高くなりがちで、

「機能凝集というより機能そのもの」みたいな形のディープモジュール(Ex. hoge_apiモジュール)

を構造的に作りやすい気がしてきています。


2. コーディングエージェントに渡すコンテキストが分散しがち

ClaudeCodeやCodex、Github Copilotを始めとするコーディングエージェントを利用してコーディングをすることが大分一般的になってきたと感じていますが、ディレクトリ分割型だと、ディレクトリごとにresource定義、module呼び出し等を実施するため、単一機能の修正のために、複数ディレクトリのコードをエージェントに渡す必要があり、コンテキストが分散しがちです。

LSPなどを利用していれば多少緩和はされると思いますが、標準的にはエージェントは特定ディレクトリ配下をfindやgrepを駆使しながらファイル参照してコード生成を行っているため、複数ディレクトリにまたがる修正の場合、精度が悪くなる傾向にあるように思います。


2.3 Terraform Workspaceでの環境管理

Terraform Workspaceを使用するパターンでは、単一のコードベースで複数環境を管理します。

※あくまで一般的な例示です。実際の構成はプロジェクトによって異なります。

components/
├── component_a/
├── component_b/
modules/
├── xx/
└── yy/
  • 単一のコードベースで複数環境を管理できる
  • モジュールを必ずしも必要としない設計が可能
  • 環境間の一貫性が保ちやすい

2.4 Terraform Workspaceの一般的な懸念点

Terraform公式ドキュメント上(When Not to Use Multiple Workspaces)でも、Workspaceを用いた永続的な環境分割は非推奨とされていますし、Web検索や生成AIへの質問をした場合も同様の意見が多いです。

主な理由として以下が挙げられます:

  • Stateファイルのbackend分離が出来ない
  • 環境ごとに異なるproviderバージョンを利用できない
  • 環境差分の吸収のためにコーディングが煩雑になる
  • workspaceの切り替えミスによる誤操作リスクが高まる

上記の指摘についてはもっともであると考えていますが、
workspaceの利便性についても見過ごせないものがあると思います。

そのため、上記問題に対しての具体的な対処方法を解説していきます。

3. よくある懸念とその対処

Terarform Workspaceが非推奨とされる理由と、それぞれへの対処方法を説明します。

3.1 「Stateファイルのbackend分離が出来ない」への対処

懸念内容

workspaceの切り替えても、stateファイル自体は同じbackendに保存されてしまい
以下のように同じS3バケットに、stateファイルが保存されてしまいます。

ある程度の規模のシステムになると、環境ごとにAWSアカウントを分けることが多いと思いますが、
workspaceの機能はあくまで一時的な環境複製のため、異なるAWSアカウントにリソースを作成することを想定していません。

AWS Account: 開発環境 (Development)
└── S3 Bucket: dev-terraform-state-bucket
    ├── env/dev/main.tfstate   # OK: 開発の状態
    ├── env/stg/main.tfstate   # ⚠️ NG: STGの状態が開発環境にある
    └── env/prd/main.tfstate   # ⚠️ NG: 本番の状態が開発環境にある

対処方法

-backend-configオプションを使用してbackendを分けましょう。
Terraformではbackendの設定自体を部分的に上書きすることが可能です。

Ex.

terraform init -backend-config=../../tfvars/<環境名>_backend.hcl
dev_backend.hcl
bucket = "xx-dev-terraform-state"
region = "ap-northeast-1"
profile = "xx-dev"

backend本体では変数は使えないですが、init時に外部ファイルで指定することで、環境ごとに別々のS3バケットを利用することが可能です。

backend.tf
terraform {
  # ここは変数が使えない
  backend "s3" {
    key                  = "component_name.tfstate"
    workspace_key_prefix = "domain_name"
  }
}

3.2 「環境ごとのproviderバージョンの管理」への対処

懸念内容

.terraform.lock.hclも環境間共有になるため、環境ごとに異なるproviderバージョンを利用できない。
ある環境で新しいproviderバージョンを利用したい場合、他の環境にも影響が及ぶ可能性がある。

対処方法

これについてはブランチワークである程度解決できます。
弊社では環境のステージに合わせてブランチを分けており、開発の進行フェーズに応じて、より上位のブランチにマージしていく形をとっています。

イメージ:

  • devブランチ(デフォルト): dev環境
  • stgブランチ: stg環境
  • prdブランチ: prd環境

そのため、開発環境では最新のproviderバージョンを利用しつつ、本番環境では安定版のproviderバージョンを利用するなどの運用が可能です。

※providerバージョンの差異を維持したいという場合は確かに対応出来ないですが、現実的にはあまりないのではないかと考えています。

異なる環境に対して適用する際は、上記のbackend変更に合わせて、
-reconfigureオプションを付与してinitを実行することで、
.terraformディレクトリの内容を最新化できます。

terraform init -reconfigure -backend-config=../../tfvars/<環境名>_backend.hcl

3.3 「環境差分の吸収のためにコーディングが煩雑になる」への対処

懸念内容

環境差分を吸収するためにコーディングが煩雑になるという懸念です。

対処方法

tfvarsを使っています。
local variablesでterraform.workspaceをキーとしたmapを定義する方法もよく見ますが、
variables側でデフォルト変数が定義出来るのと環境ごとに記述出来るので、こちらのほうが好みです。

Ex.

AWS_PROFILE=xx terraform plan -var-file=./tfvars/<環境名>.tfvars
variables.tf
variable "vpc_cidr_block" {
  type = string
}
variable "log_enabled" {
  type    = bool
  default = false
}
tfvars/dev.tfvars
vpc_cidr_block = "xxx.xxx.0.0/16"
log_enabled = true

変数化レベルで対応出来ない差分、
例えば特定環境でのみ作成したいリソースやblockについては、
count、for_each、dynamic blockを使用して対応しています。
ただ、これを書くのが辛い...というほどの差分が発生する状況になったことがないので、あまり大きな問題にはなっていません。
(むしろ環境固有の設定があるなら、コード上で表現されている方が個人的には嬉しかったり...?)


3.4 「誤操作のリスク」への対処

懸念内容

terraform workspace select の切り替えミスによる誤操作リスクについてです。
そうでなくても上記のやり方だと、オプション引数の指定ミスなども起こり得ます。

対処方法

ラッパースクリプトを書いて対応します。
workspaceや設定ファイル自体を指定するのではなく環境名を指定するだけで、
必要な処理を自動で実行するようにします。
入社したての時期に、毎度の反映作業が大変だったので数時間で(AIエージェントが)書きました。
半年以上運用していますが、今のところは誤操作もなく快適に作業できています。

以下のようにして実行しています。

tfx plan dev
tfx apply stg
参考スクリプト

※組織固有の設定が入りまくってるのでClaudeに適当に整形させました。
※実際にそのまま使えるかは確認してないのであくまで参考でお願いします。
※基本VibeCodingなので書き方も気にしてないです。

#!/bin/bash

usage() {
  echo "使用方法: tfx [-nc] [-json] [-auto-approve] [-v2] [-p プロファイル] [-t ターゲットリソース(複数指定可)] [-i issue番号 | -pr issue番号] <plan|apply|destroy> <env>"
  echo "オプション: "
  echo "  -nc    カラー出力を無効にする(plan/apply実行時に-no-colorオプションを付ける)"
  echo "  -json  JSON形式で出力する(action実行時に-jsonオプションを付ける)"
  echo "  -auto-approve  apply時に自動承認する(prd/stg環境では使用禁止)"
  echo "  -p     使用するAWSプロファイルを指定(指定がない場合は環境に応じたデフォルトプロファイルを使用)"
  echo "  -t     terraformの-targetオプションに渡すリソース名。複数指定可(例: -t resource1 -t resource2)"
  echo "  -i     GitHub issue番号にコメント投稿する(例: -i 1234) 環境変数 GITHUB_TOKEN が必要"
  echo "  -pr    現在のGitリポジトリを自動取得し、指定した番号のIssueにコメント投稿する(例: -pr 1234)"
  echo "         環境変数 GITHUB_TOKEN が必要、-iオプションと同時に指定はできません"
  echo "  -h     ヘルプメッセージを表示"
  echo "引数:"
  echo "  <plan|apply|destroy>   実行するコマンド"
  echo "  <env>          環境名"
  exit 1
}

# ========================================
# 設定セクション(プロジェクトに応じてカスタマイズ)
# ========================================

# デフォルトのAWSプロファイル設定
# 環境ごとのプロファイル名をここで定義してください
DEV_DEFAULT_PROFILE="your-dev-profile"
STG_DEFAULT_PROFILE="your-stg-profile"
PRD_DEFAULT_PROFILE="your-prd-profile"

# GitHub設定(-iオプション使用時のデフォルトリポジトリ)
GITHUB_DEFAULT_REPOSITORY="your-org/your-repo"

# 許可する環境名のリスト
ALLOWED_ENVIRONMENTS=("dev" "stg" "prd")

# auto-approveを禁止する環境のリスト
AUTO_APPROVE_FORBIDDEN_ENVS=("prd" "stg")

# ========================================
# メインスクリプト
# ========================================

# オプションと引数を分離してパース
POSITIONAL_ARGS=()
custom_profile_set=false
profile=""
targets=()
github_issue_number=""
# リポジトリ自動検出フラグ
auto_detect_repo=false
# カラー出力無効化フラグ
no_color=false
# JSON出力フラグ
json_mode=false
# auto-approveフラグ
auto_approve=false

#オプションの位置を自由にするために、最初にオプションをパースしてから位置引数をセットする
while [ $# -gt 0 ]; do
  case "$1" in
    -nc)
      no_color=true
      shift
      ;;
    -json)
      json_mode=true
      shift
      ;;
    -auto-approve)
      auto_approve=true
      shift
      ;;
    -p)
      profile=$2
      custom_profile_set=true
      shift 2
      ;;
    -t)
      targets+=("$2")
      shift 2
      ;;
    -i)
      github_issue_number=$2
      shift 2
      ;;
    -pr)
      github_issue_number=$2
      auto_detect_repo=true
      shift 2
      ;;
    -h)
      usage
      ;;
    --)
      shift
      break
      ;;
    -* )
      echo "不明なオプション: $1" >&2
      usage
      ;;
    *)
      POSITIONAL_ARGS+=("$1")
      shift
      ;;
  esac
done

# 位置引数をセット
set -- "${POSITIONAL_ARGS[@]}"

# 引数の数のバリデーション
if [ "$#" -lt 2 ]; then
  echo "引数の数が不正です。"
  usage
fi

# actionとenvを設定
action=$1
env=$2

# 第一引数(plan または apply または destroy)のバリデーション
if [ "$action" != "plan" ] && [ "$action" != "apply" ] && [ "$action" != "destroy" ]; then
  echo "コマンドは plan, apply, または destroy のみ指定可能です"
  usage
fi

# auto-approveオプションのバリデーション
if [ "$auto_approve" = true ]; then
  # apply時のみ有効
  if [ "$action" != "apply" ]; then
    echo "エラー: -auto-approveオプションはapply時のみ使用可能です"
    exit 1
  fi
  # 禁止環境チェック
  for forbidden_env in "${AUTO_APPROVE_FORBIDDEN_ENVS[@]}"; do
    if [ "$env" = "$forbidden_env" ]; then
      echo "エラー: -auto-approveオプションは${forbidden_env}環境では使用できません"
      exit 1
    fi
  done
fi

# 環境名のバリデーション
env_valid=false
for allowed_env in "${ALLOWED_ENVIRONMENTS[@]}"; do
  if [ "$env" = "$allowed_env" ]; then
    env_valid=true
    break
  fi
done

if [ "$env_valid" = false ]; then
  echo "環境は ${ALLOWED_ENVIRONMENTS[*]} のいずれかを指定してください"
  exit 1
fi

# デフォルトプロファイルの設定(カスタムプロファイルが指定されていない場合のみ)
if [ "$custom_profile_set" != true ]; then
  case "$env" in
    dev)
      profile=$DEV_DEFAULT_PROFILE
      ;;
    stg)
      profile=$STG_DEFAULT_PROFILE
      ;;
    prd)
      profile=$PRD_DEFAULT_PROFILE
      ;;
    *)
      echo "エラー: 環境 '$env' のデフォルトプロファイルが設定されていません"
      exit 1
      ;;
  esac
fi

# domainのtfvarsファイル存在チェック(常に実行)
domain_tfvars="../../tfvars/${env}.tfvars"
if [ ! -f "$domain_tfvars" ]; then
  echo "エラー: domainのtfvarsファイルが存在しません: $domain_tfvars"
  exit 1
fi

# -v2オプション指定時のcomponentのtfvarsファイル存在チェック
component_tfvars="./tfvars/${env}.tfvars"
if [ ! -f "$component_tfvars" ]; then
  echo "エラー: -v2オプションが指定されましたが、componentのtfvarsファイルが存在しません: $component_tfvars"
  exit 1
fi

echo "実行環境: $env"
echo "使用プロファイル: $profile"
if [ "$custom_profile_set" = true ]; then
  echo "カスタムプロファイルが指定されました"
fi

# IdentityCenter対応:ログイン判定し、未ログインであれば、SSOログインを実行する
if aws sts get-caller-identity --profile $profile > /dev/null 2>&1; then
  echo "✅ SSOログイン済み"
else
  echo "⚠️ SSO未ログイン、ログインを実行します..."
  if aws sso login --profile $profile; then
    echo "✅ SSOログイン成功"
  else
    echo "❌ SSOログインに失敗しました。~/.aws/configファイルを確認してください。プロファイル: $profile"
    exit 1
  fi
fi

# -target オプションの組み立て(配列として構築)
TF_TARGET_ARGS=()
if [ ${#targets[@]} -gt 0 ]; then
  for t in "${targets[@]}"; do
    TF_TARGET_ARGS+=("-target=$t")
  done
  echo "ターゲットリソース: ${TF_TARGET_ARGS[*]}"
fi

# コマンド実行
# .terraform/environment の内容と $env が一致する場合は init と workspace 選択をスキップ
current_env=""
if [ -f .terraform/environment ]; then
  current_env=$(cat .terraform/environment 2>/dev/null || echo "")
fi

if [ "$current_env" = "$env" ]; then
  echo "✅ .terraform/environment の内容が $env と一致するため、terraform init と workspace 選択をスキップします"
else
  echo "🔄 terraform init と workspace 選択を実行します(現在: '$current_env' → 対象: '$env')"

  # .terraform ディレクトリが存在する場合のみ workspace select default を実行し初期化
  if [ -d .terraform ]; then
    terraform workspace select default
  fi
  # terraform init の実行
  terraform init -reconfigure -backend-config=../../tfvars/${env}_backend.hcl -backend-config="profile=$profile" -input=false
  terraform workspace select -or-create $env
fi

# GitHub投稿用に一時ファイルの準備
if [ -n "$github_issue_number" ]; then
  current_dir_name=$(basename "$PWD")
  output_file=tmp_${env}_${current_dir_name}.txt
fi

# plan/apply
# コマンドを配列として構築(コマンドインジェクション対策)
TF_COMMAND=(terraform "$action")

# var-fileオプションの追加
TF_COMMAND+=("-var-file" "$domain_tfvars")
TF_COMMAND+=("-var-file" "$component_tfvars")

# -targetオプションの追加
if [ ${#TF_TARGET_ARGS[@]} -gt 0 ]; then
  TF_COMMAND+=("${TF_TARGET_ARGS[@]}")
fi

# その他のオプションの追加
if [ "$no_color" = true ]; then
  TF_COMMAND+=("-no-color")
fi
if [ "$json_mode" = true ]; then
  TF_COMMAND+=("-json")
fi
if [ "$auto_approve" = true ] && [ "$action" = "apply" ]; then
  TF_COMMAND+=("-auto-approve")
fi

echo "実行コマンド: AWS_PROFILE=$profile ${TF_COMMAND[*]}"
if [ -n "$github_issue_number" ]; then
  # GitHub投稿用にログを保存しながら実行
  AWS_PROFILE=$profile "${TF_COMMAND[@]}" | tee "$output_file"
  terraform_exit_code=${PIPESTATUS[0]}
else
  AWS_PROFILE=$profile "${TF_COMMAND[@]}"
  terraform_exit_code=$?
fi

# -iオプションと-prオプションが両方同時に指定されている場合はエラー
if [ "$(echo "$@" | grep -c -- "-i")" -gt 0 ] && [ "$(echo "$@" | grep -c -- "-pr")" -gt 0 ]; then
  echo "エラー: -iオプションと-prオプションは同時に使用できません。どちらかを選択してください。"
  usage
fi

# GitHub Issueコメント送信処理(planの失敗時のみスキップ)
if [ -n "$github_issue_number" ] && { [ "$terraform_exit_code" = "0" ] || [ "$action" != "plan" ]; }; then
  if [ -z "$GITHUB_TOKEN" ]; then
    echo "GITHUB_TOKENが設定されていません。環境変数にPersonal Access Tokenをセットしてください。"
    exit 1
  fi
  # GITHUB_REPOSITORYの設定
  if [ "$auto_detect_repo" = true ]; then
    # gitコマンドからリポジトリ情報を取得
    remote_url=$(git config --get remote.origin.url)
    if [ -z "$remote_url" ]; then
      echo "Gitリモートリポジトリが設定されていません。Gitリポジトリ内で実行しているか確認してください。"
      exit 1
    fi
    # リモートURLからリポジトリ名を抽出
    GITHUB_REPOSITORY=$(echo "$remote_url" | sed -E 's|^git@github.com:||; s|^https://github.com/||; s|\.git$||')
    if [ -z "$GITHUB_REPOSITORY" ]; then
      echo "GitHubリポジトリの形式が認識できません: $remote_url"
      exit 1
    fi
  else
    # デフォルト設定を使用
    GITHUB_REPOSITORY=$GITHUB_DEFAULT_REPOSITORY
  fi

  if [ -z "$GITHUB_REPOSITORY" ]; then
    echo "GITHUB_REPOSITORYが特定できませんでした。リポジトリ内で実行しているか確認してください。"
    exit 1
  fi
  # 3階層分のパスを取得
  display_path=$(pwd | awk -F/ '{
    n=NF;
    if(n>=3){print $(n-2)"/"$(n-1)"/"$n}
    else if(n==2){print $(n-1)"/"$n}
    else{print $n}
  }')
  # ヒアドキュメントを使用して正しいMarkdown形式でコメント本文を作成
  # シングルクォートのEOFを使うことで変数展開とコマンド置換を防止
  # ただし、display_path変数はあらかじめ展開しておく
  # ANSIカラーコードを除去してGitHub上での表示を改善(-no-colorオプションだと標準出力も見にくくなるので)
  terraform_output=$(cat "$output_file" | sed 's/\x1b\[[0-9;]*m//g')
  comment_body=$(cat <<'EOF_COMMENT'
### Terraformの実行結果

TF_COMMAND_PLACEHOLDER


<details><summary>DISPLAY_PATH_PLACEHOLDER</summary>

TERRAFORM_OUTPUT_PLACEHOLDER


</details>
EOF_COMMENT
)
  # プレースホルダーを実際の値に置換
  comment_body="${comment_body//TF_COMMAND_PLACEHOLDER/${TF_COMMAND[*]}}"
  comment_body="${comment_body//DISPLAY_PATH_PLACEHOLDER/$display_path}"
  comment_body="${comment_body//TERRAFORM_OUTPUT_PLACEHOLDER/$terraform_output}"
  # jqでJSONエスケープ
  json_body=$(printf '%s' "$comment_body" | jq -Rs '{body: .}')

  api_url="https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${github_issue_number}/comments"
  echo "GitHubイシュー #${github_issue_number} にコメントを投稿しています..."

  # リクエストを送信し、それをteeコマンドで変数と標準出力に分岐
  html_url=$(curl -s \
    -X POST \
    -H "Authorization: token $GITHUB_TOKEN" \
    -H "Accept: application/vnd.github+json" \
    -H "X-GitHub-Api-Version: 2022-11-28" \
    -d "$json_body" \
    "$api_url" | jq '.html_url')

  # html_urlが取得できていれば成功
  if [ -n "$html_url" ]; then
    echo "GitHubイシューへのコメント投稿が成功しました : ${html_url}"
  else
    echo "GitHubイシューへのコメント投稿が失敗しました"
  fi

  # 一時ファイルを削除
  rm -f "$output_file"
elif [ -n "$github_issue_number" ] && [ "$terraform_exit_code" != "0" ] && [ "$action" = "plan" ]; then
  echo "Terraform planが失敗したため、GitHub issueへのコメント送信をスキップしました (終了コード: $terraform_exit_code)"
  # 一時ファイルを削除
  rm -f "$output_file"
fi

# Terraformコマンドの終了コードで終了
exit "$terraform_exit_code"


この手のラッパーを書くコストがコーディングエージェントの普及で相当に下がっているので、
Terragruntなどの既存のラッパーツールを利用せず、自分の組織に適合する形で組んでしまうほうが制御可能で運用的には楽だなと感じています。
(Terragruntは優れたツールだと思いますが、現時点の自分の組織規模・運用体制ではオーバースペックに感じています。)

4. Terraform Workspace運用をしてみて

上記で示した対処法(backend分離、環境差分の吸収、ラッパースクリプト化)を適用した結果、以下のような感じになりました。

4.1 結果としてのディレクトリ構成

ざっくり以下のような構成になっています。

domain_a/
├── components/
│   ├── component_a/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── backend.tf
│   │   └── tfvars/         #コンポーネント固有の環境変数
│   │       ├── dev.tfvars
│   │       ├── stg.tfvars
│   │       └── prd.tfvars
│   ├── component_b/
│   │   └── tfvars/
│   └── ...
├── tfvars/                 #コンポーネント共通の環境変数、backend設定
│   ├── dev.tfvars
│   ├── stg.tfvars
│   ├── prd.tfvars
│   ├── dev_backend.hcl
│   ├── stg_backend.hcl
│   └── prd_backend.hcl
└── modules/
    ├── xx/
    └── ...

ポイント

  • 環境設定ファイル(tfvars)を配置して環境変数を管理
  • Terraform本体のコード(main.tf等)は環境間で共有
  • backend設定(backend.hcl)で環境ごとのS3バケットを分離

4.3 Terraform Workspace使用のメリット

Terarform Workspaceを使用することでディレクトリ分割型と比較して、以下のメリットが得られました:

DRY原則の適用

  • 環境ごとに個別にresourceを書く必要が原則ない
  • 環境ごとのモジュール呼び出しコードの記述も不要
  • 結果的にコード量が大幅に削減

コンテキストの集約

  • 環境間のコードベースが同一ディレクトリに存在
  • 環境差分の認知がしやすい
  • AIエージェント(Claude Code、Copilot等)利用時のコンテキスト分散を回避

モジュール設計の最適化

  • モジュールを「異なる環境での再利用」のためではなく、「同一環境内での共通パターンの再利用」のために設計可能
  • 環境間再利用の制約から解放され、モジュール化しない or より適切な粒度でのモジュール化が可能に
  • サブモジュールも現状不使用

モジュール設計で悩むことが大幅に減り、加えて結果的にコードがDRYになっていることで、日々の作業は非常に楽になりました。

5. Terraform Workspaceを使うべき場合/使わない場合

5.1 Terraform Workspaceが向いているケース

以下の条件を満たす場合、Workspaceアプローチが適していると思います:

  • 環境間でインフラ構成が大きく変わらない
  • 開発速度を重視したい
  • 環境数が多い
  • 自前のスクリプトをメンテナンスすることに抵抗がない

※環境またぎのクロスアカウント連携の課題:

例えば「本番データをマスクしてSTGに同期する」といったケースでは、Workspace運用だと本番側にexport、STG側にimportのリソースを用意する形になり、本来一体のものが分離されてしまいます。
このような環境間連携の場合には、workspaceとは別の仕組みのほうが良さそうです。

5.2 ディレクトリ分割が向いているケース

以下の条件を満たす場合、ディレクトリ分割が適していますと思います:

  • 環境ごとにインフラ構成が大きく異なる
  • Terraformを扱う人が多い(できるだけTerraformの標準的な利用に寄せたい)
  • 環境数が少ない
  • 完全な分離を最優先したい

開発環境と本番環境で大きく構成が異なる場合(例: 開発はEC2を1台、本番だけ冗長化してALB構成など)は、ディレクトリ分割のほうが適しています。

6. まとめ

Terraform Workspaceによる複数環境管理は、適切な対処を施すことで十分実用的な選択肢となり得るのではないでしょうか?検討段階において非推奨と言われているからという理由で選択肢から除外するには、もったいない機能だなというのが再評価してみた感想です。

自分の所属する組織に限っての話にはなりますが、今のところ利用にあたり大きな問題はなく、むしろモジュール設計の難易度低下やDRY原則の適用、コーディングエージェント利用時のコンテキスト集約など、多くのメリットが享受できています。

巷のベストプラクティスに囚われすぎず、自分たちの組織・プロジェクトの特性に合った方法を選択することが重要であると感じました。

誰かのTerraformの設計における意思決定の一助となれば幸いです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?