Terraformを使い始めると、わりと早い段階で「stateファイル、どこに置くの?」という壁にぶつかります。自分もしばらくローカルのterraform.tfstateでやり過ごしてたんですが、チームで使ったり、CI/CDに組み込もうとした瞬間に詰みました。今回はそのstateファイルをAWS S3で管理する「S3バックエンド」の設定について、実際に手を動かしながら整理してみます。
このシリーズ「Terraform × AWS インフラ自動化入門」では、Terraformの基礎からAWS上での実践的な使い方まで、全4回で順を追って扱っていく予定です。第1回の今回はその根っこになるbackend設定から。
- 第1回:StateファイルをS3で管理するバックエンド設定(← 今ここ)
- 第2回:モジュール分割とディレクトリ構成
- 第3回:GitHub Actionsと組み合わせたCI/CD構築
- 第4回:複数環境(dev/stg/prod)の管理戦略
この記事でわかること
- Terraform stateファイルの役割と、ローカル管理の問題点
- S3バックエンド設定の全体像と必要な要素
- S3バケットの作成とバージョニング設定
- backend設定ブロックの書き方と各パラメータの意味
- ロック機能(use_lockfile と DynamoDB)の選択方法
- IAM権限設定と環境ごとのstate分割方法
Terraform stateファイルとは何か(軽くおさらい)
Terraformは「現在のインフラの状態」をterraform.tfstateというJSONファイルに記録しています。このファイルがあるからこそ、「このリソースはもう作ってある」「これは変更が必要」と判断できる。
ローカルに置いておく場合、自分一人で作業する分には問題ないですが、こんな問題が起きます:
- 複数人で作業すると誰かのローカルにしかstateがない
- うっかり
.gitignoreに入れ忘れてGitHubに上げてしまう(認証情報も入ってることがある) - CI/CDから実行できない
そこで使うのが「リモートバックエンド」で、AWSならS3 + ロック機能の組み合わせが定番です。
S3バックエンドの全体像
構成はシンプルで、以下の2〜3要素を用意します。
- S3バケット:stateファイルの保存場所
- ロック機能:並行実行によるstate破壊を防ぐ
- バージョニング:state壊したときに戻れるようにする(必須級)
自分が最初にやらかしたのは「stateバケットをTerraformで管理しようとする」やつです。鶏と卵で、当然初回initが失敗します。stateバケット自体は、Terraformとは別手段(手動や別のブートストラップ手順)で先に作っておくのが一般的です。
S3バケットを事前に作る
AWSコンソールでもCLIでもOK。ここではCLIで。
# バケット作成(バケット名はグローバルユニークが必要)
aws s3api create-bucket \
--bucket my-terraform-state-20260701 \
--region ap-northeast-1 \
--create-bucket-configuration LocationConstraint=ap-northeast-1
# バージョニングを有効化
aws s3api put-bucket-versioning \
--bucket my-terraform-state-20260701 \
--versioning-configuration Status=Enabled
# パブリックアクセスブロック(念のため)
aws s3api put-public-access-block \
--bucket my-terraform-state-20260701 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
バージョニングを有効にすることで、stateファイルの履歴が保持され、問題が起きたときに以前のバージョンへ戻せるようになります。ほぼ必須の設定です。
backendブロックの書き方
Terraformの設定ファイル(backend.tfやmain.tf)に以下を追加します。
terraform {
required_version = ">= 1.11.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state-20260701"
key = "envs/dev/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true
}
}
各パラメータの意味:
bucket:さっき作ったS3バケット名key:S3内のパス。環境ごとに分けておくと後で楽region:バケットのリージョンencrypt:サーバーサイド暗号化(trueにしておく)use_lockfile:S3バックエンドのロック機能。並行実行を防ぐ
これを書いたらterraform initを実行。成功するとS3にstateが管理されるようになります。
$ terraform init
Initializing the backend...
Successfully configured the backend "s3"!
Terraform will automatically use this backend unless the backend
configuration changes.
ロック機能の話:DynamoDB vs use_lockfile
ここが2025〜2026年時点で一番変化があったポイントです。
従来、Terraformでロックを実現するにはDynamoDBテーブルを別途作成する必要がありました。LockIDというパーティションキーを持つテーブルを用意して、dynamodb_tableパラメータに指定する、という手順です。
# 旧来の方法
backend "s3" {
bucket = "my-terraform-state-20260701"
key = "envs/dev/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-lock-table"
}
ただし、TerraformのS3バックエンドではS3ネイティブのロック機能(use_lockfile)が利用でき、これをtrueにするとDynamoDBなしでロックできるようになります。use_lockfileは2024年以降のバージョンで推奨される方法として公式ドキュメントにも記載されています。
use_lockfile = trueにすると、S3バケット内にロック用のファイル(ロックオブジェクト)が生成され、並行実行による競合を防ぎます。実装の詳細(内部的な条件付き書き込みやヘッダーなど)については環境によって差が出る可能性があるので、困ったら実際のバケット内を見て挙動を確認するのが確実です。
既存プロジェクトでDynamoDBベースのロックを継続するか、use_lockfileへ寄せるかは運用要件(既存ツールとの互換性など)で判断になると思いますが、新規ならDynamoDBのテーブル管理が不要な構成は便利です。
移行する場合(DynamoDB → use_lockfile)
既存のプロジェクトでdynamodb_tableを使っている場合の移行は、backend.tfを書き換えてからterraform init -reconfigureを実行するだけです。
# dynamodb_tableを消してuse_lockfileを追加
backend "s3" {
bucket = "my-terraform-state-20260701"
key = "envs/dev/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true
}
$ terraform init -reconfigure
移行後、DynamoDBテーブルが不要になったら削除してOK。ただし他のシステムやTerraform設定から参照していないか確認してから。
IAM権限の設定
EC2やCodeBuild、GitHub Actions(OIDC)からTerraformを実行する場合、実行ロールにS3への権限が必要です。最小権限の原則でいうと、バケット本体にはs3:ListBucket、オブジェクトへはs3:GetObject・s3:PutObject・s3:DeleteObjectが必要です。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::my-terraform-state-20260701"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-terraform-state-20260701/*"
}
]
}
use_lockfileを有効にした場合、ロックオブジェクトに対しても上記権限が必要です。ワイルドカード(/*)を使っていれば自動的にカバーされます。
クレデンシャルをbackendブロックに直書きするのはNGです。環境変数やAWS CLIの設定、IAMロール、CI/CD専用のIAMユーザーを使って認証するようにしましょう。
環境ごとにstateを分ける
開発・ステージング・本番で同じstateを共有するのはリスクが高すぎます。keyパラメータを環境・アカウント・ドメイン(例:network、data、app)ごとに分けておくことで、ロックの競合を減らし、planの速度を上げ、誤操作の影響範囲を限定できます。
# dev環境
key = "envs/dev/terraform.tfstate"
# staging環境
key = "envs/staging/terraform.tfstate"
# prod環境
key = "envs/prod/terraform.tfstate"
ただ「backendブロックに変数は使えない」という制約があるので(ここは最初ハマります)、partial configurationという方法で対応するのが一般的です。
# backend.tf(共通部分だけ書いておく)
terraform {
backend "s3" {
# 環境ごとに異なる値はここには書かない
}
}
# backend-dev.hcl
bucket = "my-terraform-state-20260701"
key = "envs/dev/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true
# initのときにファイルを指定する
terraform init -backend-config=backend-dev.hcl
環境ごとにhclファイルを用意しておいて、init時に-backend-configフラグでファイルを指定する方式です。GitHub Actionsから実行するときもこのパターンが使いやすいです。
まとめ
S3バックエンドの設定自体はそんなに難しくないんですが、いくつか押さえておくべきポイントがあります。
- バケットを先に別手段で用意するのが一般的(Terraformで管理しない)
use_lockfileが現在推奨される選択肢(DynamoDB不要)- backendブロックに変数は使えないため、partial configurationで対応
- 環境ごとにstateを分ける(keyパラメータで分割)
- IAM権限は最小権限の原則で設定
ここさえ押さえておけば、次のステップ(モジュール化やCI/CD統合)へ進みやすくなります。
📚 シリーズ「Terraform × AWS インフラ自動化入門」(第1回 / 全4回)
→ 次回の記事: 【第2回】Terraform × AWS インフラ自動化入門 — VPC・EC2・RDSをコードで構築するリソース定義の書き方

