チームでAWSのインフラをTerraformで管理しはじめると、最初はうまく回っているように見えても、プロジェクトが大きくなるにつれて「どこに何を書けばいいか分からなくなった」「tfstateが衝突してデプロイが止まった」「誰かが誤って本番を変更してしまった」といった問題が次々と出てきます。
Terraformそのものの使い方はすぐ覚えられます。しかし「チームで長期運用できる設計」になるかどうかは、tfstateの分割戦略・モジュールの切り方・ワークスペースの使い分けという3つの設計判断にかかっています。
この記事では、オンプレミスのインフラをAWSに移行しながらTerraformを導入したエンジニアが直面する設計上の壁を、具体的な構成例とともに解説します。

なぜTerraformの「設計」が難しいのか
Terraformは宣言的にインフラを記述できる強力なツールですが、コードの書き方に制約がほとんどありません。そのためチームに設計規約がないと、書く人によってディレクトリ構成もモジュールの粒度もバラバラになり、やがてコードの全体像を誰も把握できなくなります。
オンプレ環境でのインフラ管理に慣れたエンジニアがTerraformに移行する際、特につまずきやすいのが次の3点です。
・tfstateの扱い: ローカルにstateファイルを置いたままチームで使うと、変更が衝突してインフラ定義とステートが乖離する
・モジュールの粒度: 最初から汎用モジュールを作ろうとして複雑になりすぎる、あるいは逆に何もモジュール化しないでコードが重複しまくる
・環境管理: 開発・ステージング・本番で同じTerraformコードを使う方法が分からず、それぞれ別にコードを書いてしまう
これらをまとめて解消するのが、本記事で解説する3つの設計パターンです。
設計の前提:Terraformバージョンとバックエンド選定
1. Terraformバージョンの固定
Terraformはバージョン間で書き方が変わることがあります。チームで利用するバージョンは required_version で固定し、tfenv などで統一管理するのが現場のセオリーです。
terraform { required_version = ">= 1.6.0, < 2.0.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } }
プロバイダーバージョンも同様に固定します。`~> 5.0` と書くと5.x系の最新パッチにアップデートしつつ、メジャーバージョンアップは防げます。
2. リモートバックエンドの設定(必須)
チームで使う場合、tfstateはローカルに置いてはいけません。S3バックエンド+DynamoDBロックが、AWSではデファクトスタンダードです(2026年8月時点)。
terraform { backend "s3" { bucket = "mycompany-terraform-state" key = "prod/network/terraform.tfstate" region = "ap-northeast-1" dynamodb_table = "terraform-state-lock" encrypt = true } }
| 設定項目 | 役割 | 設定例 |
|---|---|---|
| S3バケット | stateファイルの保存先 | バージョニング有効・暗号化必須 |
| key(パス) | stateファイルの識別子 | {環境}/{コンポーネント}/terraform.tfstate |
| DynamoDB テーブル | 同時実行のロック制御 | LockID 属性(String型)で作成 |
| encrypt | S3側でのSSE暗号化を強制 | true 固定 |
DynamoDBのロックを使わないと、CI/CDから同時に `terraform apply` が走ったとき、stateが壊れるリスクがあります。必ずセットで設定してください。
tfstate分割:何を境界にするか
「1リポジトリ・1stateファイル」から始めた場合、リソースが増えるにつれて `terraform plan` の実行時間が長くなり、1か所の変更で全リソースへの影響を確認しないといけなくなります。これがstateを分割する主な理由です。
推奨:レイヤー×環境でstateを分割する
現場で使われる典型的な分割境界は次の2軸です。
・レイヤー(責務): ネットワーク(VPC/サブネット)、セキュリティ(IAM/Security Group)、アプリ(EC2/ECS/RDS)のように責務で分ける
・環境: dev・staging・prod ごとに別のstateファイルを持つ
“`
terraform/
environments/
dev/
network/ ← dev環境のネットワークstate
app/ ← dev環境のアプリstate
prod/
network/ ← prod環境のネットワークstate
app/ ← prod環境のアプリstate
modules/
vpc/
ecs-cluster/
rds/
“`
この構成にすると、例えばアプリチームがECSのタスク定義を変更するとき、ネットワークのstateには触れません。変更範囲が明確になり、`plan` の出力も見やすくなります。
stateを分割したときのデータ参照
分割したstate間でリソースIDを参照したい場面が出てきます(例: ネットワークstateのサブネットIDをアプリstateから参照する)。これは `terraform_remote_state` または Systems Manager Parameter Store(SSM)で解決できます。
SSMを使う方法のほうが、TerraformのバージョンやバックエンドURLへの依存を減らせるため、長期運用には向いています。
# network側: サブネットIDをSSMに書き出す resource "aws_ssm_parameter" "subnet_id" { name = "/myapp/prod/network/subnet_id" type = "String" value = aws_subnet.private.id } # app側: SSMからサブネットIDを取得する data "aws_ssm_parameter" "subnet_id" { name = "/myapp/prod/network/subnet_id" }
モジュール化:再利用しやすいコードの切り方
モジュールを作るべきタイミング
Terraformのモジュール化でよくある失敗は「最初から何でもモジュールにしようとする」です。モジュールを作るのに最適なタイミングは、「同じリソース構成を2か所以上で使う」ことが確定したときです。1か所だけなら、まずはフラットなコードで書いておくほうがシンプルです。
モジュールの構造
モジュールの標準的なファイル構成は次のとおりです。
modules/ ecs-service/ main.tf # リソース定義 variables.tf # 入力変数 outputs.tf # 出力値 versions.tf # required_providers(任意だが推奨) README.md # 使い方と変数説明
変数の設計:デフォルト値の置き方
モジュールの変数にデフォルト値を設定するかどうかは、悩ましい判断です。現場では次の基準が使われます。
・デフォルト値を設定する: 環境によらず固定できる設定(例: ヘルスチェック間隔の秒数、ログ保持期間)
・デフォルト値なし(必須引数)にする: 環境ごとに必ず違う値(例: サブネットID、インスタンスタイプ)
デフォルト値を多用すると、呼び出し側でどんな値が使われているか分かりにくくなります。迷ったら必須引数にして、呼び出し元に明示させるほうが安全です。
モジュールのバージョン管理
社内共有モジュールをGitで管理する場合、モジュール呼び出し時にタグ(バージョン)を明示します。
module "ecs_service" { source = "git::https://github.com/myorg/terraform-modules.git//ecs-service?ref=v1.2.0" cluster_name = "prod-cluster" desired_count = 2 subnet_ids = data.aws_ssm_parameter.subnet_id.value }
`?ref=v1.2.0` でタグを固定することで、モジュールのアップデートが自動で反映されて意図しない変更が起きる事故を防げます。
ワークスペース vs ディレクトリ:環境分離のどちらを選ぶか
Terraformには `terraform workspace` という機能があり、1つのコードベースで複数環境(dev/staging/prod)を切り替えられます。しかし現場の経験則として、本番を含む環境分離にはワークスペースより別ディレクトリ構成のほうが安全です。
| ワークスペース | ディレクトリ分割 | |
|---|---|---|
| stateの分離 | 同じバックエンドの別キーに分離 | 明示的に別パスで完全分離 |
| 誤操作リスク | 高(workspace切り忘れで本番を変更しやすい) | 低(ディレクトリを移動する操作が必要) |
| 変数の管理 | workspace名で条件分岐が必要で複雑になりやすい | .tfvarsファイルを環境ごとに用意して明快 |
| 向いているケース | 一時的な機能ブランチのテスト環境など | dev/staging/prod の長期運用環境 |
`terraform.workspace == “prod”` のような条件分岐はコードを読みにくくするうえ、切り忘れによる誤適用リスクを高めます。環境ごとに `.tfvars` ファイルを分けて明示的に渡すほうが安全で見通しが良いです。
# environments/prod/app/prod.tfvars instance_type = "t3.medium" desired_count = 3 min_capacity = 2 max_capacity = 10 # 実行時に環境変数ファイルを明示指定 terraform apply -var-file="prod.tfvars"
CI/CDパイプラインとの連携
自動化の基本フロー
Terraform運用が安定してくると、CI/CDに組み込んで変更のレビュー→適用を自動化したくなります。GitHub Actionsを使う場合の典型的なフローは次のとおりです。
・PRオープン時: `terraform fmt` のチェック → `terraform plan` を実行してPRコメントに差分を表示
・mainブランチへのマージ時: `terraform apply` を自動実行(本番は手動承認ステップを挟む)
本番適用の承認ステップ
本番環境への `terraform apply` は、人の目視確認を挟むことを強くお勧めします。GitHub Actionsなら `environment: production` に承認者を設定することで、適用前に指定したメンバーの承認を必須にできます。
# .github/workflows/terraform-prod.yml(抜粋) jobs: apply: runs-on: ubuntu-latest environment: production # ← Environmentに承認者を設定 steps: - name: Terraform Apply run: terraform apply -auto-approve -var-file="prod.tfvars"
よくあるトラブルと対処法
「stateが壊れた」ときの対処
DynamoDBのロックが残ったまま中断した場合、次回の `plan` や `apply` 時に “Error acquiring the state lock” が出ます。`terraform force-unlock
「リソースが勝手に再作成されそうになる」
`terraform plan` で意図しない `destroy + create` が出るのは、多くの場合リソース識別子(ARN/IDなど)の変更や、`lifecycle` ブロックの設定不足が原因です。DBやElasticacheのように再作成コストが高いリソースは `prevent_destroy = true` を設定しておくと、事故時のバリアになります。
resource "aws_db_instance" "main" { # ... lifecycle { prevent_destroy = true } }
「既存リソースをTerraform管理下に取り込みたい」
コンソールで作成済みのリソースをTerraformに取り込む場合は `terraform import` を使います。ただしインポートした直後は `plan` で差分が出ることが多いので、`.tf` ファイルの定義を実際のリソース状態に合わせて調整してからコミットしてください。Terraform 1.5以降では `import` ブロックをコードとして書けるようになり、インポート操作をレビュー可能にできます。
# Terraform 1.5以降: importブロック(コードとしてレビュー可能) import { to = aws_s3_bucket.legacy id = "my-existing-bucket-name" }

本記事のまとめ
チームでTerraformを長期運用するための設計ポイントをまとめます。
| 設計の観点 | 推奨アプローチ | 避けるべきパターン |
|---|---|---|
| stateの場所 | S3+DynamoDBロックのリモートバックエンド | ローカルstateをGitにコミット |
| stateの分割 | レイヤー(ネットワーク/アプリ)×環境で分割 | 1つのstateにすべてのリソースを詰め込む |
| モジュール化 | 2か所以上で再利用するときに作る・バージョンタグ固定 | 最初から何でもモジュール化しようとする |
| 環境分離 | ディレクトリ分割+.tfvarsで環境別変数を管理 | workspaceに全環境を詰め込んで条件分岐 |
| CI/CD | PRで plan、マージ後に apply・本番は承認ステップ必須 | 全環境にauto-applyを設定する |
Terraformの設計にはプロジェクトの規模やチームの慣習によって正解が変わる部分もあります。ここで紹介したパターンを出発点にして、自チームの状況に合わせて調整してみてください。
IaCツール選定に迷っている場合は、AWS CloudFormationとの比較をまとめた記事も参考にしてください。
AWS CloudFormation vs Terraform 徹底比較|IaCツール選定で失敗しないための実践ガイド
また、開発・ステージング・本番の環境分離設計をアカウント分割の観点から整理した記事も合わせてお読みいただくと、Terraform設計の土台となるインフラ構成がより深く理解できます。
AWS開発・ステージング・本番の環境分離設計|アカウント分割 vs VPC分割のコスト・セキュリティ・運用比較と現場の判断基準
PR
AWSのアーキテクチャ設計原則から実装パターンまでを体系的に解説。Terraform等のIaCと組み合わせて読むことで、コードに落とし込む前の設計思考が整理できます。
