terraform remote state bootstrap

August 19, 2026

Fujikeeeen

web development

Platform Engineering MVP Phase 2 bootstrap — Terraform remote state を先に用意する

Phase 2 で必要になる S3 + DynamoDB / Azure Storage の bootstrap 手順。鶏と卵問題をどう解くか

Post last updated: August 19, 2026

はじめに

Phase 2 構想記事 で書いた通り、
ローカル Docker から AWS ECS / Azure App Service に行くと Terraform の state をリモートに置く のが現実的になる。

Phase 1 なら generated/terraform.tfstate が手元にあれば足りた。
Phase 2 では S3 + DynamoDB(AWS)か Storage Account(Azure)が先に要る。

これが bootstrap — 「Terraform 本体より先に、state を置く場所だけ1回用意する」手順。

まだ Phase 2 本体は未実装。bootstrap 手順のメモ。
リポジトリ README に載せる前に、ブログに整理しておく。

bootstrap とは何か

terraform apply  →  state を S3 に保存したい
                      ↓
                   でも S3 バケットがまだない
                      ↓
                   先にバケットを作る = bootstrap

bootstrap で作るもの(AWS の例):

リソース用途
S3 バケットterraform.tfstate の保存先
DynamoDB テーブルstate の ロック(同時 apply 防止)
(任意)KMSstate の暗号化

Phase 2 の README イメージ:

bootstrap/          … アカウント基盤(初回1回、人間 or 管理者)
domain/service.edn  … アプリの意図(Compiler の入力)
generated/          … アプリのインフラ + backend 参照

bootstrap 自体は service.edn には入れない ことが多い。
state バケットはアカウント単位で1回、アプリ定義はサービス単位、だから。

AWS bootstrap(CLI 手動)

いちばんわかりやすい方法。初回だけ実行。

1. 変数を決める

export AWS_REGION=ap-northeast-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export TF_STATE_BUCKET="platform-mvp-tfstate-${ACCOUNT_ID}"
export TF_STATE_LOCK_TABLE="platform-mvp-tfstate-lock"
export TF_STATE_KEY="env/dev/platform-mvp-web/terraform.tfstate"

2. S3 バケット作成

aws s3api create-bucket \
  --bucket "$TF_STATE_BUCKET" \
  --region "$AWS_REGION" \
  --create-bucket-configuration LocationConstraint="$AWS_REGION"

# バージョニング(state 復旧用)
aws s3api put-bucket-versioning \
  --bucket "$TF_STATE_BUCKET" \
  --versioning-configuration Status=Enabled

# 公開ブロック
aws s3api put-public-access-block \
  --bucket "$TF_STATE_BUCKET" \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

3. DynamoDB ロックテーブル作成

aws dynamodb create-table \
  --table-name "$TF_STATE_LOCK_TABLE" \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --region "$AWS_REGION"

4. 生成 Terraform に backend を足す

Phase 2 の Compiler が generated/main.tf.json に backend ブロックを埋め込む想定:

{
  "terraform": {
    "backend": {
      "s3": {
        "bucket": "platform-mvp-tfstate-123456789012",
        "key": "env/dev/platform-mvp-web/terraform.tfstate",
        "region": "ap-northeast-1",
        "dynamodb_table": "platform-mvp-tfstate-lock",
        "encrypt": true
      }
    }
  }
}

service.edn:backend を足して Compiler が埋める、という設計もアリ:

{:platform :aws
 :region "ap-northeast-1"
 :backend
 {:bucket "platform-mvp-tfstate-123456789012"
  :dynamodb-table "platform-mvp-tfstate-lock"
  :key "env/dev/platform-mvp-web/terraform.tfstate"}}

5. 通常フロー(2回目以降)

clojure -M:generate
cd generated
terraform init -reconfigure
terraform plan
terraform apply

AWS bootstrap(Terraform 版)

CLI より本番っぽくするなら、リポジトリに bootstrap 専用ディレクトリ を置く:

platform-engineering-mvp/
  bootstrap/aws/          ← state は local のみ
    main.tf               ← S3 + DynamoDB だけ
  generated/              ← ECS 本体(remote state 使用)
    main.tf.json

bootstrap/aws/main.tf の概念:

resource "aws_s3_bucket" "tfstate" {
  bucket = "platform-mvp-tfstate-${data.aws_caller_identity.current.account_id}"
}

resource "aws_dynamodb_table" "tfstate_lock" {
  name         = "platform-mvp-tfstate-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

実行順:

cd bootstrap/aws
terraform init
terraform apply          # ← この state だけ local

cd ../../generated
terraform init -reconfigure
terraform apply          # ← ここから S3 backend

注意: bootstrap 用 Terraform は 自分自身の state を同じ S3 に置けない(鶏と卵)。
bootstrap だけ local state、または別アカウントの state バケット、が定石。

Azure bootstrap(折りたたみ)

:platform :azure で行く場合。S3 の代わりに Storage Account + Blob Container

RG=platform-mvp-bootstrap
SA=platformmvptfstate   # グローバル一意、小文字英数字のみ
CONTAINER=tfstate

az group create -n $RG -l japaneast
az storage account create -g $RG -n $SA -l japaneast --sku Standard_LRS
az storage container create --name $CONTAINER --account-name $SA

backend 設定(azurerm):

terraform {
  backend "azurerm" {
    resource_group_name  = "platform-mvp-bootstrap"
    storage_account_name = "platformmvptfstate"
    container_name       = "tfstate"
    key                  = "dev/platform-mvp-web.terraform.tfstate"
  }
}

Azure も bootstrap/azure/ に小さな Terraform / CLI スクリプトを置く形になる。

Phase 2 README に書く章立て(案)

## Prerequisites(Phase 2)
- AWS CLI / Azure CLI 認証済み
- bootstrap 済み(初回のみ)

## Bootstrap(初回のみ)
→ 詳細: https://www.theshibsters.com/blog/platform-engineering-mvp-phase2-bootstrap

### AWS
1. `scripts/bootstrap-aws-state.sh` を実行
2. 出力された bucket / table 名を確認
3. `clojure -M:generate` で backend 付き Terraform JSON を生成

### 通常フロー(2回目以降)
clojure -M:generate → cd generated → terraform init → plan → apply

よくある落とし穴

落とし穴対策
bootstrap を service.edn に混ぜるアカウント基盤とアプリ定義を分離
bootstrap も remote state にするbootstrap だけ local state
terraform init 忘れbackend 変更後は init -reconfigure
state バケットを公開Public Access Block 必須
ロックなしチーム/CI 運用なら DynamoDB ロック推奨

おわりに

Phase 2 で増えるのは Adapter 差し替え だけじゃなくて、
「state を置く場所を先に1回作る」 という Day-0 ステップ。

Phase 1 の cd generated && terraform apply の前に、
bootstrap → generate → init → apply という順番が増える。

実装再開するとき、この記事と Phase 2 構想 をセットで README にリンクする予定。

関連リンク

Author

Fujikeeeen

All about Fujikeeeen and what he does and where he works