Terraform Bootstrap — 鶏卵問題の解決
Terraform を使うと、最初にぶつかる構造的な問題が 「state ファイルをどこに置くか」。本記事ではその解決策である S3 backend + DynamoDB Lock 構成と、それを「Terraform 自身では作れない」鶏卵問題、そして bootstrap シェルスクリプトでの解消について書きます。
そもそも state とは
Terraform は .tf ファイルに「望ましい状態」を書きます。実際の AWS の状態は刻々と変わるので、Terraform は「いま自分が管理しているリソース」を記録するファイルを持っています。これが state(terraform.tfstate)です。
state はマシンの中で「現実とコードの差分計算」に使われます。これが無いと terraform plan ができません。
state をローカルに置くとマズい理由
初心者向けチュートリアルでは terraform.tfstate がローカルに作られます。これを本番運用するとすぐに事故ります:
- 消えると詰む: マシン故障や誤って
rmしたら、AWS 上のリソースを「Terraform が知らないもの」として扱う羽目になる - 共有不可: チームで Terraform を回せない(複数人が独自の state を持つことになる)
- 同時 apply の事故: 複数人が同時に
terraform applyすると state が壊れる - 機密値が含まれる: state には IAM Role ARN や ACM 証明書 ARN などが平文で入る → ローカルに置くとうっかり commit する
S3 backend + DynamoDB Lock 構成
これらの問題を解決する定石が、Terraform の S3 backend + DynamoDB Lock です:
- S3 バケット: tfstate ファイルを保存。バージョニング ON でロールバック可能
- DynamoDB テーブル:
terraform apply中に排他ロックを取り、同時実行を防ぐ
本シリーズでは以下の名前で作りました:
| 用途 | 名前 | 設定 |
|---|---|---|
| state バケット | iigtn-tfstate-XXXXXXXXXXXX | バージョニング ON, AES256, Public Access Block 全 ON |
| ロックテーブル | iigtn-tflock | PK = LockID (String), On-demand |
S3 バケット名にアカウント ID を入れているのは、S3 バケット名は AWS 全体でグローバル一意 という制約があるため。アカウント ID を suffix にするのが慣例です。
鶏卵問題
ここで問題が発生します。S3 バケットと DynamoDB テーブルを Terraform で作ろうとすると…
これが Terraform 始めの定番の鶏卵問題です。S3 backend を初めて使う人がここで詰まります。
解決策: bootstrap だけ AWS CLI で手動
解決法はシンプル: state を置く S3 バケットと DynamoDB テーブルだけは AWS CLI で手動で作る。これら 2 個は Terraform で管理しません。それ以降のすべてのリソースは Terraform で管理します。
本シリーズでは、これを terraform/bootstrap/setup.sh という idempotent なシェルスクリプトにまとめました。何度実行しても安全(既に存在すればスキップ)。
setup.sh の中身
#!/usr/bin/env bash
set -euo pipefail
REGION="${REGION:-ap-northeast-1}"
ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
BUCKET="iigtn-tfstate-${ACCOUNT_ID}"
TABLE="iigtn-tflock"
# ── S3 バケット作成 (idempotent) ────────────────────
if aws s3api head-bucket --bucket "$BUCKET" 2>/dev/null; then
echo "[skip] $BUCKET already exists"
else
aws s3api create-bucket \
--bucket "$BUCKET" \
--region "$REGION" \
--create-bucket-configuration LocationConstraint="$REGION"
fi
# バージョニング ON
aws s3api put-bucket-versioning \
--bucket "$BUCKET" \
--versioning-configuration Status=Enabled
# 暗号化 (AES256)
aws s3api put-bucket-encryption \
--bucket "$BUCKET" \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
# Public Block 全 ON
aws s3api put-public-access-block \
--bucket "$BUCKET" \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,\
BlockPublicPolicy=true,RestrictPublicBuckets=true
# ── DynamoDB テーブル作成 (idempotent) ──────────────
if aws dynamodb describe-table --table-name "$TABLE" --region "$REGION" >/dev/null 2>&1; then
echo "[skip] $TABLE already exists"
else
aws dynamodb create-table \
--table-name "$TABLE" \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region "$REGION"
aws dynamodb wait table-exists --table-name "$TABLE" --region "$REGION"
fi
echo "Done."
実行手順
AWS CLI のプロファイルを prod に切り替えて、まず aws sts get-caller-identity で確認してから実行します:
export AWS_PROFILE=aws-prod
aws sts get-caller-identity
# Account: XXXXXXXXXXXX (期待通りの prod アカウントか確認)
bash terraform/bootstrap/setup.sh
実行後の確認:
aws s3 ls | grep iigtn-tfstate
# 2026-04-25 12:00:00 iigtn-tfstate-XXXXXXXXXXXX
aws dynamodb list-tables --region ap-northeast-1
# "TableNames": ["iigtn-tflock", ...]
Terraform 側の backend 設定
bootstrap が終わったら、Terraform の各環境ディレクトリに backend.tf を書きます:
# terraform/envs/prod/backend.tf
terraform {
backend "s3" {
bucket = "iigtn-tfstate-XXXXXXXXXXXX"
key = "envs/prod/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "iigtn-tflock"
encrypt = true
}
}
key は state ファイルのパス。envs/prod/ と envs/dev/ で別 key にすれば、同じバケットに複数環境の state を置けます。
var.xxx)が使えません。アカウント ID 等もハードコードする必要があります。これは Terraform の仕様で、init 時に backend が確定している必要があるためです。
terraform init で接続
cd terraform/envs/prod
terraform init
init が成功すると、Terraform は iigtn-tfstate-XXXXXXXXXXXX バケットの envs/prod/terraform.tfstate を state として使うようになります。
destroy したくなった時
Terraform で作ったリソースは terraform destroy で消せますが、bootstrap の S3 + DynamoDB は Terraform 管理外なので手動で消します:
# DynamoDB テーブル削除
aws dynamodb delete-table --table-name iigtn-tflock --region ap-northeast-1
# S3 バケット中身削除 (バージョン付きなので version も消す必要)
aws s3 rm "s3://iigtn-tfstate-XXXXXXXXXXXX" --recursive
aws s3api delete-objects --bucket iigtn-tfstate-XXXXXXXXXXXX \
--delete "$(aws s3api list-object-versions \
--bucket iigtn-tfstate-XXXXXXXXXXXX \
--output json \
--query '{Objects: Versions[].{Key: Key, VersionId: VersionId}}')"
# バケット削除
aws s3api delete-bucket --bucket iigtn-tfstate-XXXXXXXXXXXX
State Lock の効果を体感する
setup.sh の DynamoDB テーブルは「同時に 2 人が terraform apply しないようにする」仕組みです。試しに同じディレクトリで 2 つの Git Bash を開き、両方で同時に terraform plan を打つと、片方が次のメッセージで待たされます:
Acquiring state lock. This may take a few moments...
ロックが取れた方が処理を進め、もう片方は待つか諦めます。これが効いていれば、複数人が同時に apply して state が壊れる事故は起きません。
Lock が取れない時の force-unlock
Terraform が異常終了して Lock が残ってしまった時:
terraform force-unlock <LOCK_ID>
Lock ID はエラーメッセージに表示されます。それでも消えなければ、DynamoDB テーブルの該当アイテムを直接削除します(最終手段)。
次の記事
Bootstrap が終わったら、いよいよ terraform init をやります… が、実は Windows + Git Bash 環境だと、ここで 「TLS bad record MAC」 という見たことのないエラーが出てプロバイダがダウンロードできない事故に遭遇しました。
次の記事ではその原因究明と回避策について書きます。Windows で Terraform を使うすべての人に役立つ内容です。
📚 用語集
- state (tfstate)
- Terraform が「いま自分が管理しているリソースの状態」を記録する JSON ファイル。
terraform.tfstate。これが無いと差分計算ができない。 - backend
- Terraform が state ファイルを保管する場所の設定。ローカルファイル / S3 / Terraform Cloud などがある。本シリーズでは S3 を使う。
- S3 バケット
- S3 のファイル保管単位。名前は AWS 全体で一意でなければならない。
- バージョニング (Versioning)
- S3 オブジェクトの旧バージョンを保持する機能。state ファイルがこわれた時に旧版に戻せる。tfstate バケットでは必須。
- SSE-S3 (AES256)
- S3 がデフォルトで提供する保存時暗号化。AWS 管理キーで完全無料。tfstate には機密値が含まれるので暗号化必須。
- Public Access Block
- S3 バケットの「うっかり public 化」を構造的に防ぐ 4 つのスイッチ。tfstate バケットでは全部 true にする。
- DynamoDB Lock
- Terraform の S3 backend オプションで、DynamoDB テーブルを使って state ロックを取る仕組み。同時 apply 防止。
- PAY_PER_REQUEST (On-demand)
- DynamoDB の課金モード。読み書き回数で課金。アクセス薄いなら最安。
- idempotent (冪等)
- 「何度実行しても同じ結果になる」性質。bootstrap スクリプトは idempotent にしておくと、エラー時の再実行が安全。
- 鶏卵問題
- 「A を作るには B が必要、B を作るには A が必要」という相互依存。bootstrap を Terraform 管理外にすることで解決する。
- terraform init
- Terraform の初期化コマンド。プロバイダ(AWS など)のダウンロード、backend への接続、モジュールの取得を行う。
- terraform plan
- 「いま apply したら何が変わるか」を表示するコマンド。実際の変更はしない、安全に何度でも流せる。
- terraform apply
- plan の内容を実際に AWS に反映するコマンド。state lock を取る。
- terraform destroy
- Terraform 管理下のリソースをすべて削除するコマンド。本番運用後は基本的に使わない。
- terraform force-unlock
- 異常終了等で残った state lock を強制的に解除するコマンド。Lock ID を引数で指定する。
- aws s3api
- AWS CLI の S3 低レベル API。
aws s3(高レベル)と違って、バケットレベルの設定(バージョニング・暗号化・Public Block 等)はこちらで操作する。