はじめに
個人開発しているWebアプリのAWSインフラをTerraformで管理しているが、リソースが156個まで膨らみ、1つのstateファイルに全部入りの状態になっていた。terraform plan が遅い、CloudFrontを変更するだけなのにRDSのdiffが気になる、という問題を抱えていた。
この記事では、Terramateを使ってモノリスstateを8つのスタックに分割し、さらにS3 native locking(Terraform 1.10+)でDynamoDBロックテーブルも廃止した手順を書く。
📌 この記事で扱うこと:
- Terramateのスタック設計とコード生成の仕組み
-
terraform state mvによるゼロダウンタイムのstate移行手順 - S3 native locking(
use_lockfile = true)でDynamoDB不要にする方法 - ハマったポイントと回避策
環境
| 項目 | バージョン |
|---|---|
| Terraform | >= 1.10.0 |
| Terramate | v0.12.x |
| AWS Provider | ~> 5.0 |
| Backend | S3(ap-northeast-1) |
実装
スタック設計 — 何を基準に分割するか
156リソースをどう分割するかが最初の設計判断。以下の基準で8スタックに切った:
- 変更頻度が違うものは分ける(VPCはほぼ不変、ECSは頻繁に変更)
- blast radiusを小さくする(DB変更がCDNに影響しないように)
- 依存方向を一方向にする(network → database → ecs → cdn の順)
stacks/prod/
├── network/ # VPC, サブネット, NAT Gateway
├── dns/ # Route53, ACM証明書
├── database/ # Aurora PostgreSQL, Secrets Manager
├── ecs/ # ECS Fargate, ALB, ECR, IAM
├── cdn/ # CloudFront, S3 (frontend)
├── scheduler/ # EventBridge Scheduler
├── mwaa/ # Apache Airflow (MWAA)
└── monitoring/ # CloudWatch Alarms, SNS
Terramate設定 — 3ファイルで全部回る
Terramateで必要なのは主に3種類のファイル。
① globals.tm.hcl — 全スタック共通の変数:
globals {
s3_bucket = "my-terraform-state-bucket"
s3_region = "ap-northeast-1"
s3_use_lockfile = true
}
② generate.tm.hcl — backend設定とcross-stack参照の自動生成:
generate_hcl "_backend.tf" {
content {
terraform {
backend "s3" {
bucket = global.s3_bucket
key = global.s3_key
region = global.s3_region
encrypt = true
use_lockfile = global.s3_use_lockfile
}
}
}
}
generate_hcl "_remote.tf" {
condition = tm_length(tm_try(global.remote_states, {})) > 0
content {
tm_dynamic "data" {
for_each = global.remote_states
iterator = rs
labels = ["terraform_remote_state", rs.key]
content {
backend = "s3"
config = {
bucket = global.s3_bucket
key = rs.value
region = global.s3_region
}
}
}
}
}
③ 各スタックの stack.tm.hcl — スタック固有の設定:
# stacks/prod/ecs/stack.tm.hcl
stack {
name = "my-app-prod-ecs"
description = "Production ECS Fargate, ALB, ECR, IAM"
tags = ["prod", "ecs"]
}
globals {
s3_key = "prod/ecs/terraform.tfstate"
remote_states = {
network = "prod/network/terraform.tfstate"
database = "prod/database/terraform.tfstate"
dns = "prod/dns/terraform.tfstate"
}
}
terramate generate を実行すると、各スタックに _backend.tf と _remote.tf が自動生成される:
// _remote.tf(自動生成)
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-terraform-state-bucket"
key = "prod/network/terraform.tfstate"
region = "ap-northeast-1"
}
}
スタック間の参照ボイラープレートを手書きしなくていいのがTerramateの嬉しいところ。
S3 native locking — DynamoDBがいらなくなった
Terraform 1.10で use_lockfile = true が追加された。これまでS3 backendでstate lockingするにはDynamoDBテーブルが必須だった:
# Before: DynamoDBテーブル必須
backend "s3" {
bucket = "my-bucket"
key = "terraform.tfstate"
dynamodb_table = "my-lock-table" # ← これがないとlockが取れない
}
# After: use_lockfile = true だけでOK
backend "s3" {
bucket = "my-bucket"
key = "terraform.tfstate"
use_lockfile = true # S3のConditional Writesでロック
}
S3のConditional Writes機能を使ってロックを取る仕組みで、DynamoDBテーブルの作成・管理が完全に不要になる。個人開発ではDynamoDBのコスト自体は微小だが、管理対象が1つ減るのは地味にありがたい。
注意: use_lockfile = true を使うには Terraform 1.10以上が必要。required_version を忘れずに設定すること。
terraform {
required_version = ">= 1.10.0"
}
state移行 — terraform state mv をPhase分割で実行
既存モノリスstateから各スタックへのリソース移動は terraform state mv で行う。ここが一番神経を使うフェーズ。
移行スクリプトをPhase分割で構成した:
#!/usr/bin/env bash
set -euo pipefail
# Phase 0: バックアップ
terraform state pull > backup/original-state.json
# Phase 1〜8: 各スタックごとにリソースを移動
# 例: Phase 1 — networkスタック
state_mv() {
local stack="$1"
local old_addr="$2"
terraform state mv \
-state-out="backup/${stack}.tfstate" \
"${old_addr}" "${old_addr}"
}
# networkスタックのリソースを移動
state_mv "network" "data.aws_availability_zones.available"
state_mv "network" "module.vpc"
# 新stateをS3にpush
cd "stacks/prod/network"
terraform init -reconfigure
terraform state push "../../backup/network.tfstate"
# zero diff確認
terraform plan -detailed-exitcode
# exit code 0 = 差分なし、2 = 差分あり(NG)
全8スタック分を繰り返す。各Phaseの後に必ず terraform plan -detailed-exitcode でzero diffを確認してから次に進む。
CI対応 — GitHub Actionsでterramate run
CIも terraform validate を各スタックで実行するように更新:
- name: Setup Terramate
uses: terramate-io/terramate-action@v2
- name: Terraform Validate
run: |
cd terraform
terramate generate
terramate run -- terraform init -backend=false
terramate run -- terraform validate
terramate run は全スタックに対してコマンドを一括実行する。変更のあったスタックだけ対象にしたい場合は terramate run --changed が使える。
ハマったポイント
1. s3_key をglobalsのデフォルトに書いてはいけない
最初、globals.tm.hcl に s3_key のデフォルト値を書いてしまった。
# ❌ 絶対にやるな
globals {
s3_key = "prod/terraform.tfstate" # 全スタックが同じkeyを使ってしまう
}
こうすると全スタックが同じstateファイルを読み書きして、スタック同士がお互いのリソースを破壊する。s3_key は必ず各スタックの stack.tm.hcl で個別に定義すること。
2. terraform state mv の -state-out は累積する
state_mv を同じスタック向けに複数回実行する場合、-state-out で指定したファイルにリソースが追記される(上書きではない)。これは意図通りの動作だが、途中でやり直す場合は出力先のファイルを削除してから再実行する必要がある。
3. terraform_remote_state で参照するスタックは先にinitが必要
cross-stack参照を使うスタック(例: ecsがnetworkを参照)は、参照先のスタックが先にS3上にstateを持っている必要がある。移行の実行順序が重要で、依存グラフのリーフ(network)から順に移行する。
4. required_version >= 1.10.0 を忘れると use_lockfile が無視される
Terraform 1.9以下では use_lockfile パラメータが認識されず、エラーにもならずに静かに無視される。ロックが取れない状態で plan / apply が走るので、チーム開発だと事故の元になる。required_version で明示的に下限を設定しておくのが安全。
まとめ
-
Terramate はスタック分割とコード生成に特化したツールで、学習コストが低い。
globals.tm.hcl/generate.tm.hcl/stack.tm.hclの3種類を理解すれば使い始められる - S3 native locking でDynamoDBテーブルの管理が不要に。Terraform 1.10以上なら使わない理由がない
- state移行は
terraform state mvのPhase分割 + 各Phase後のzero diff確認で安全に進められる - HCP Terraform(旧Terraform Cloud)を使わなくても、個人開発〜小規模チームなら十分に運用できる構成
次のステップとして、terramate run --changed をCIに組み込んで、PRで変更されたスタックだけ plan を実行するようにしたい。
🏀 NBA Trade Tracker: https://www.nba-iso-flow.com/