はじめに
Terraformをゼロから学びながら、その内容を自分なりに整理して残していく入門連載です。
第2回では、入力変数と出力値を使ってTerraform設定を使いやすくしました。
Terraformは、作成したリソースと設定の対応をState(ステート)に記録しています。第1回と第2回ではローカルStateのまま進めていましたが、Stateがどこに保存され、どのように共有されるのかも理解しておきたいと感じました。ローカルファイルで管理するStateは個人で試すには手軽ですが、チームで共有したり、端末をまたいで作業したりする場合には向いていません。気軽に扱うと、いつの間にか「どの端末のStateが正しいんだっけ?」となりそうです。
今回は、AWS S3をリモートバックエンドとして使い、Stateをリモートで管理します。State保存用のS3バケットは、通常のインフラとは別のTerraform構成で作成します。
この記事は学習の記録を兼ねています。内容に誤りや、よりよい方法があればコメントで教えていただけると助かります。
今回のゴール
以下をできるようにすることが目標です。
- Terraform Stateの役割を理解する
- ローカルStateの内容を確認する
- State保存用S3バケットをbootstrap構成で作成する
- S3リモートバックエンドへStateを移行する
- S3のロックファイルによる排他制御を有効にする
前提条件
以下の準備ができていることを前提に進めます。
- 第2回の
terraform-aws-learningディレクトリがある - AWS CLIとTerraformを利用できる
- AWS CLIで認証済みである
-
aws sts get-caller-identityで操作対象のAWSアカウントを確認済みである
第1回と第2回は以下を参照してください。
Terraform Stateとは
Terraform Stateは、Terraformが管理するリソースの情報を記録するデータです。既定では、作業ディレクトリにterraform.tfstateというJSONファイルとして保存されます。
Terraformはplanやapplyの実行時に、設定ファイル、State、AWS上の実際のリソースを比較して、必要な変更を判断します。Stateがなければ、Terraformは設定上のリソースと実際のリソースを対応付けられません。
Stateには、リソースIDやARNだけでなく、設定値が含まれる場合があります。sensitive = trueを指定した値も、表示はマスクされますが、Stateから自動的に除外されるわけではありません。そのため、Stateは機密情報として扱い、Gitへコミットしないでください。
ローカルStateを確認する
第2回で作成したディレクトリへ移動します。
cd terraform-aws-learning
Stateで管理しているリソースは、次のコマンドで確認できます。
terraform state list
前回のSSMパラメータを作成済みであれば、次のように表示されます。
aws_ssm_parameter.example
特定リソースのStateを確認するには、terraform state showを使います。
terraform state show aws_ssm_parameter.example
State全体を人間が読みやすい形式で確認するには、terraform showも利用できます。
terraform show
これらのコマンドはStateを読み取るだけで、AWSリソースは変更しません。ただし、出力に値が含まれる可能性があるため、画面共有やログの取り扱いには注意します。
S3リモートバックエンドを使う理由
Stateをローカルファイルで管理すると、複数人がそれぞれ別のStateを持つことになります。この状態で同じインフラを変更すると、意図しない差分や競合が起きるおそれがあります。
S3リモートバックエンドを使うと、StateをS3に保存し、Terraformを実行するたびに同じStateを参照できます。use_lockfile = trueを指定すると、更新中はS3にロックファイルが作成され、同時更新を防止できます。
今回は次のように構成します。
terraform-aws-learning/
├── main.tf
├── variables.tf
├── outputs.tf
├── backend.tf
├── backend.hcl
└── bootstrap/
└── main.tf
- ルートディレクトリ: 第2回で作成したSSMパラメータを管理する構成
-
bootstrap: State保存用S3バケットだけを管理する構成 -
backend.hcl: S3バックエンドの具体的な接続先を記述するファイル
S3バケットを作るTerraform設定と、そのS3バケットにStateを保存するTerraform設定を同じStateで管理すると、最初にStateをどこへ保存するかという循環が発生します。そのため、State用バケットは別のbootstrap構成で先に作成します。Stateの置き場所を作るためのStateの置き場所が必要になる、少しややこしいところです。
最初はState用バケットまで同じmain.tfに書こうと考えました。しかし、バックエンドはterraform initの時点で必要になるため、バケットが先に存在していなければなりません。役割を分けることで、初回の手順も追いやすくなりました。
State用S3バケットを作成する
バケット名を決める
S3バケット名はAWS全体で一意である必要があります。アカウントIDとリージョンを含めた名前を使います。
名前付きプロファイルを利用している場合は、アカウントIDを取得する前に同じAWS_PROFILEを設定してください。
export AWS_PROFILE=admin
export AWS_REGION=ap-northeast-1
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export TF_STATE_BUCKET="terraform-state-${AWS_ACCOUNT_ID}-${AWS_REGION}"
echo "$TF_STATE_BUCKET"
bootstrap/main.tfを作成する
State保存用のS3バケットを作成します。
mkdir -p bootstrap
bootstrap/main.tfを次の内容で作成します。
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.55"
}
}
}
variable "aws_region" {
description = "AWS region for the state bucket"
type = string
}
variable "state_bucket_name" {
description = "Globally unique name for the Terraform State bucket"
type = string
}
provider "aws" {
region = var.aws_region
}
resource "aws_s3_bucket" "terraform_state" {
bucket = var.state_bucket_name
force_destroy = false
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
output "state_bucket_name" {
description = "Name of the S3 bucket for Terraform State"
value = aws_s3_bucket.terraform_state.bucket
}
この構成では、次の設定を有効にしています。
| 設定 | 目的 |
|---|---|
| バージョニング | 過去のStateへ復旧できるようにする |
| SSE-S3暗号化 | S3へ保存するStateを暗号化する |
| Public Access Block | Stateバケットの公開を防止する |
prevent_destroy |
Terraformによるバケットの誤削除を防止する |
force_destroy = false |
オブジェクトが残るバケットの削除を防止する |
prevent_destroyを設定すると、後でterraform destroyを実行してもバケットは削除できません。検証用のSSMパラメータは最後に削除しますが、State用バケットまで一緒に消してしまわないよう、ここでは保護を優先します。うっかり削除したあとにStateの行方を捜索するのは避けたいところです。
bootstrap構成を適用する
bootstrapディレクトリへ移動し、初期化・検証・適用を実行します。
cd bootstrap
terraform fmt -check
terraform init
terraform validate
terraform apply \
-var="aws_region=${AWS_REGION}" \
-var="state_bucket_name=${TF_STATE_BUCKET}"
確認プロンプトでyesを入力します。正常に完了すると、作成したバケット名が出力されます。
Outputs:
state_bucket_name = "terraform-state-123456789012-ap-northeast-1"
この時点では、bootstrap自身のStateはローカルに保存されています。通常のインフラ用Stateを保存するバケットを作ることが目的なので、ここでは問題ありません。
リモートバックエンドを設定する
ルートディレクトリへ戻ります。
cd ..
backend.tfを作成する
backend.tfを作成します。
terraform {
backend "s3" {}
}
バックエンド設定内では通常の入力変数を参照できません。そのため、バケット名などの値は次に作成するbackend.hclから渡します。
backend.hclを作成する
backend.hclを作成し、YOUR_BUCKET_NAMEを先ほど作成したバケット名に置き換えます。
bucket = "YOUR_BUCKET_NAME"
key = "terraform-aws-learning/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true
例えば、バケット名がterraform-state-123456789012-ap-northeast-1の場合は、次の内容です。
bucket = "terraform-state-123456789012-ap-northeast-1"
key = "terraform-aws-learning/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true
keyは、バケット内でStateを保存するパスです。複数のTerraform構成で同じバケットを使う場合は、構成ごとに異なるkeyを指定します。同じkeyを使うと、別構成のStateが同居してしまうため、ここはフォルダ分けと同じ感覚で丁寧に決めます。
encrypt = trueはS3へ保存するStateの暗号化を要求します。バケットのデフォルト暗号化とあわせて、Stateの暗号化を明示します。
StateをS3へ移行する
既存のローカルStateをS3へ移行するため、-migrate-stateを付けて初期化します。
terraform init \
-migrate-state \
-backend-config=backend.hcl
Stateのコピーを確認するプロンプトが表示された場合は、内容を確認してyesを入力します。
初期化後、Stateに含まれるリソースを確認します。
terraform state list
terraform output
第2回で作成したSSMパラメータが表示されれば、リモートStateへの移行は完了です。私は移行直後にterraform state listとterraform outputの両方を実行し、これまで確認していたリソースと出力値が変わらず参照できることを確認しました。terraform.tfstateのローカルコピーは削除せず、移行の確認が済むまで保管しておくと安心です。ただし、Stateには機密情報が含まれる可能性があるため、Gitへコミットしないでください。
S3上のStateとロックを確認する
StateがS3に保存されていることを確認します。
aws s3 ls "s3://${TF_STATE_BUCKET}/terraform-aws-learning/"
次のようにterraform.tfstateが表示されます。
2026-09-21 12:00:00 1234 terraform.tfstate
terraform applyやterraform destroyなど、Stateを書き換える操作中は、同じパスにterraform.tfstate.tflockが一時的に作成されます。
terraform-aws-learning/
├── terraform.tfstate
└── terraform.tfstate.tflock
ロックファイルはTerraformが正常に終了すると自動で削除されます。別の端末で同じStateを更新しようとすると、ロック取得待ちまたはError acquiring the state lockとなり、同時更新を防止できます。
普段はロックファイルがすぐに消えるため、S3コンソールで確認できないこともあります。私はまず、use_lockfile = trueがバックエンド設定に含まれていることを確認できれば十分と考えています。複数人で運用する段階になったら、別のターミナルから同時に操作してロックの挙動を確認してみる予定です。
異常終了後にロックが残った場合でも、他の人やプロセスがTerraformを実行していないことを十分に確認するまでは削除してはいけません。安全を確認できた場合だけ、エラーメッセージに表示されたロックIDを使って次のコマンドを実行します。
terraform force-unlock <LOCK_ID>
今後のTerraform操作
バックエンドの初期化が完了すれば、これまでと同じようにTerraformを実行できます。
terraform plan
terraform apply
terraform destroy
destroyはSSMパラメータを削除しますが、State保存用のS3バケットは削除しません。S3バケットは以後のTerraform構成でもStateを保存する基盤として使い続けます。destroyと入力すると少し身構えますが、対象をplanで確認してから実行すれば大丈夫です。
バックエンド設定を変更した場合は、再度terraform initを実行してください。
まとめ
今回は、Terraform StateをS3リモートバックエンドで管理しました。
- TerraformはStateを使って、設定と実際のリソースを対応付ける
- Stateには機密情報が含まれる可能性があるため、Gitへコミットしない
- State用S3バケットは、通常のインフラとは別のbootstrap構成で作成する
- S3バケットではバージョニング、暗号化、Public Access Blockを有効にする
-
terraform init -migrate-stateでローカルStateをS3へ移行できる -
use_lockfile = trueでS3のロックファイルによる同時更新の防止を有効にできる
StateをS3へ移しても、普段のplanやapplyの操作方法は大きく変わりません。一方で、StateはTerraformがインフラを管理するための重要な情報だと、ローカルからリモートへ移行する手順を通じて実感できました。
次回は、データソースを使ってAWSから読み取り専用の情報を取得し、Terraform設定で利用する方法を学ぶ予定です。





