- 1 AWS×Terraform 複数人開発の基盤 — state管理・lock・drift対策ハンズオン
- 1.1 1. この記事について
- 1.2 2. 複数人Terraform開発で何が壊れるか — state競合・apply競合・drift事例
- 1.3 3. リモートbackend再入門(複数人視点)— S3+DynamoDB lock の役割と限界
- 1.4 4. 環境分離戦略の比較 — workspace vs ディレクトリ分離 vs Terragrunt
- 1.5 5. state competition ハンズオン — 2人同時 apply を体験する
- 1.6 6. drift検知の運用 — 定期 terraform plan + GitHub Actions + Slack通知
- 1.7 7. state破損からの復旧 — 強制unlock・state pull/push・rm/import
- 1.8 8. まとめと次のステップ
AWS×Terraform 複数人開発の基盤 — state管理・lock・drift対策ハンズオン
AWS×Terraform 複数人開発シリーズ
- 第1弾(本記事): 複数人開発の基盤 — state管理・lock・drift対策
- 第2弾(公開済み): PR駆動CI/CD — GitHub Actions+OIDCで複数人レビューフローを構築
- 第3弾(公開済み): AWS CodePipeline×CodeBuildで構築するTerraform CI/CD
関連シリーズ(前提知識):
Git/GitHub × Terraform 実践シリーズ(全5弾)も合わせてどうぞ:
1. この記事について
1-1. 本シリーズの位置付け
前作(Terraform実践・Git/GitHub第4弾セキュリティ)でTerraform IaCとセキュリティ運用の基礎は固まった。本シリーズはこれを複数人開発の現場で運用するために必須の知識を扱う。
本シリーズ「AWS×Terraform 複数人開発運用編」は全3弾構成で、チーム開発特有の課題をステップごとに解消していく。
| 弾 | テーマ | 内容 |
|---|---|---|
| 第1弾(本記事) | state管理・lock・drift対策 | 複数人開発で壊れるポイントを体験し、S3+DynamoDBで防ぐ |
| 第2弾 | PR駆動CI/CD | GitHub Actions+OIDCで複数人レビューフローを構築 |
| 第3弾 | CodePipeline×CodeBuild | AWSネイティブなTerraform CI/CDパイプラインを構築 |
1-2. 前作(ID:1208)との差別化
同じTerraform×state管理というテーマでも、本記事と前作では対象読者・フォーカスが異なる。
| 観点 | 前作(ID:1208) | 本記事 |
|---|---|---|
| 対象 | 個人開発者 | チーム・複数人開発チーム |
| state移行 | ローカル→リモートstate移行手順が中心 | リモートstateを前提に、競合・破損・driftを体験 |
| ロック | DynamoDBロックの設定方法を解説 | ロックがない場合の破壊シナリオを実演し、体験的に理解 |
| CI/CD | 触れない | 本シリーズ第2〜3弾で段階的に構築 |
| 主なゴール | IaC基礎の習得 | チーム運用の実践力習得 |
前作を読んでいる方は、本記事で「なぜそれが必要なのか」を体験として再確認できる。未読でも、Terraform基礎(init / plan / apply)を理解していれば問題なく進められる。
1-3. 対象読者・前提知識
対象読者
- Terraformをチームで使い始めた、または使い始めようとしているエンジニア
- 個人での
terraform apply経験はあるが、チーム運用でのトラブルを経験したことがない方 - CI/CDパイプラインにTerraformを組み込みたいと考えているインフラ・DevOpsエンジニア
前提知識
以下の知識を持っていることを前提としている。未習得の場合は先に前作シリーズを参照されたい。
- Terraform基礎:
init/plan/apply/destroyの実行経験 - GitHub基礎: ブランチ・PR・マージフローの理解
- AWS基礎: IAM・S3・DynamoDB・EC2の概念理解
1-4. この記事で学べること
本記事を通じて、以下のスキルと知識を習得できる。
- state競合の再現: 複数人が同時に
terraform applyした場合に実際に起きるエラーを体験する - DynamoDBロックの仕組みを体感: ロックがない状態・ある状態の両方を試し、なぜ必要かを理解する
- apply競合のシナリオ把握: 同一リソースに対して2人が別の変更を加えた場合の破壊的な結果を知る
- コードレビューの重要性を理解: レビューなし
applyが招くインシデントのパターンを学ぶ - driftの発生と検知: AWSコンソールで手動変更を加えた後、
terraform planで差分を確認する - drift対策の実装:
terraform refreshと定期的なplanチェックの組み込み方を知る - S3+DynamoDBによるリモートstateのベストプラクティス: チーム運用に耐えるbackend設定を構築する
1-5. 必要なもの
ハンズオンを進めるにあたり、以下の環境を用意しておくこと。
AWSアカウント
- 管理者権限またはTerraform実行用IAMユーザー(S3・DynamoDB・EC2の操作権限)
- ハンズオンで作成するリソースはすべて削除コマンドを示す(コスト最小化)
ローカル環境
# バージョン確認
terraform version
# → Terraform v1.5.0 以上を推奨
aws --version
# → aws-cli/2.x 以上
git --version
# → git 2.x 以上
GitHubアカウント
- 第2弾以降で使用(本記事では不要)
2. 複数人Terraform開発で何が壊れるか — state競合・apply競合・drift事例
2-1. なぜ複数人開発でTerraformは壊れるのか
Terraformはstateファイル(terraform.tfstate)が真実の源(source of truth)として機能する。このファイルには現在のインフラ構成が記録されており、plan / apply はすべてこのファイルと現実のAWSリソースの差分を計算する仕組みになっている。
個人開発では1人しかこのファイルを操作しないため問題は起きにくい。しかしチームで使うと、次の3つの問題が同時に発生しうる。
- state競合: 2人が同時にstateファイルを読み書きする
- apply競合: 同一リソースに対して2人が別の変更を
applyする - drift: AWSコンソールでの手動変更がstateに反映されない
それぞれを具体的なエラーログと実例で見ていく。
2-2. 課題1: state競合 — 2人が同時に apply した場合
シナリオ
AチームとBチームのエンジニアが同時に別々の変更を terraform apply しようとした。リモートstateバックエンドにDynamoDBロックが設定されていない場合、何が起きるか。
Aさんのターミナル
$ terraform apply
Terraform used the selected providers to generate the following execution plan.
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Bさんのターミナル(ほぼ同時に実行)
$ terraform apply
Terraform used the selected providers to generate the following execution plan.
...
Error: Failed to persist state to backend
The error shown above has prevented Terraform from writing the updated state
to the configured backend. To allow for recovery, the state has been written
to the file "errored.tfstate" in the current working directory.
Running "terraform apply" again at this point will not work. You must first
rectify the issue above, then manually copy the file "errored.tfstate" back
to its expected location:
s3://my-terraform-state/terraform.tfstate
この状態では、Bさんのローカルに errored.tfstate が残り、S3上のstateとローカルが乖離する。次回 plan / apply 時に意図しないリソース削除・再作成が発生するリスクがある。
DynamoDBロックが有効な場合
DynamoDBロックが設定されていれば、2人目のアクセスは即座にブロックされる。
$ terraform apply
Acquiring state lock. This may take a few moments...
Error: Error locking state: Error acquiring the state lock: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: 8f5a9d0c-3b2e-4c1a-a7e3-0f9b6d2e1c4f
Path:s3://my-terraform-state/terraform.tfstate
Operation: OperationTypeApply
Who: user-A@hostname
Version:1.5.7
Created:2024-01-15 09:23:41.123456789 +0000 UTC
Info:
Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.
このエラーが出た場合、Bさんはロックが解放されるまで待つ(またはAさんにロック解放を依頼する)ことになる。破壊的な状態乖離は発生しない。
2-3. 課題2: apply競合 — DynamoDBロックがない場合の破壊シナリオ
シナリオ
DynamoDBロックなし・S3バックエンドのみという構成で、2人のエンジニアが異なるリソース変更を同時に apply した場合を考える。
初期状態(S3上のstate)
{
"resources": [
{
"type": "aws_instance",
"name": "web",
"instances": [
{
"attributes": {
"instance_type": "t3.micro",
"tags": {
"Environment": "staging"
}
}
}
]
}
]
}
Aさんの変更: instance_type を t3.micro → t3.small に変更して apply
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.small" # ← 変更
tags = {
Environment = "staging"
}
}
Bさんの変更: tags.Environment を staging → production に変更して apply(Aさんとほぼ同時)
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Environment = "production" # ← 変更
}
}
結果
| タイミング | S3 state の内容 | 実際のAWSリソース |
|---|---|---|
| Apply前 | t3.micro / staging | t3.micro / staging |
| Aさんapply完了後 | t3.small / staging | t3.small / staging |
| Bさんapply完了後 | t3.micro / production | t3.micro / production |
Bさんが古いstateをベースに apply したため、Aさんの変更(t3.small)が上書きされて消えた。 さらにBさんのstate(t3.micro / production)が最終stateになるため、次回の plan では差分がゼロと表示され、問題に気づきにくい。
$ terraform plan
No changes. Your infrastructure matches the configuration.
このような「サイレントな上書き」がロックなし運用の最大のリスクである。
2-4. 課題3: review不在 — コードレビューなしで apply が通る危険性
シナリオ
小規模チームでは「個人リポジトリのように main ブランチに直接 push して apply」という運用がされることがある。
# 開発者が直接 main に push
$ git add main.tf
$ git commit -m "fix: EC2のinstance_type変更"
$ git push origin main
# その直後に apply
$ terraform apply -auto-approve
この運用では以下のリスクが生じる。
パターン1: count の誤削除
# 変更前
resource "aws_instance" "worker" {
count= 3
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
# 変更後(countを誤って削除)
resource "aws_instance" "worker" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
$ terraform plan
Terraform will perform the following actions:
# aws_instance.worker[0] will be destroyed
- resource "aws_instance" "worker" {
- id= "i-0abc123def456" -> null
...
}
# aws_instance.worker[1] will be destroyed
- resource "aws_instance" "worker" {
- id= "i-0abc123def457" -> null
...
}
# aws_instance.worker[2] will be destroyed
- resource "aws_instance" "worker" {
- id= "i-0abc123def458" -> null
...
}
# aws_instance.worker will be created
+ resource "aws_instance" "worker" {
...
}
Plan: 1 to add, 0 to change, 3 to destroy.
-auto-approve 付きで実行した場合、本番の3台のEC2が削除されてから1台が起動する。レビューがあれば plan の destroy 件数で気づける。
パターン2: 誤ったリソース名変更によるリソース再作成
# 変更前
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs"
}
# 変更後(リソース名のリファクタリングのつもり)
resource "aws_s3_bucket" "application_logs" {
bucket = "my-app-logs"
}
$ terraform plan
# aws_s3_bucket.logs will be destroyed
- resource "aws_s3_bucket" "logs" { ... }
# aws_s3_bucket.application_logs will be created
+ resource "aws_s3_bucket" "application_logs" { ... }
Plan: 1 to add, 0 to change, 1 to destroy.
S3バケット名は同じでも、Terraformのリソースアドレスが変わるため削除→再作成と解釈される。バケット内のデータが消える前にレビューで気づく仕組みが不可欠だ。
2-5. 課題4: drift — 手動コンソール変更による状態乖離の具体例
driftとは
Terraformが管理しているリソースを、AWSコンソール・AWS CLIで直接変更すると、Terraformのstateと実際のリソースの間に乖離(drift)が発生する。
具体例: セキュリティグループの手動変更
Terraformで管理しているセキュリティグループの定義
resource "aws_security_group" "web" {
name = "web-sg"
description = "Web server security group"
vpc_id= aws_vpc.main.id
ingress {
from_port= 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port= 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port= 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
AWSコンソールで手動追加したルール
担当者がデバッグのために AWSコンソールから SSH(ポート22)のインバウンドルールを追加した。
インバウンドルール追加:
タイプ: SSH
プロトコル: TCP
ポート: 22
ソース: 0.0.0.0/0 ← 本番環境では危険
drift発生後の terraform plan 出力
$ terraform plan
aws_security_group.web: Refreshing state... [id=sg-0abc123def456789]
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_security_group.web will be updated in-place
~ resource "aws_security_group" "web" {
id= "sg-0abc123def456789"
name = "web-sg"
# (other attributes hidden)
- ingress {
- cidr_blocks= ["0.0.0.0/0"] -> null
- from_port = 22 -> null
- protocol= "tcp" -> null
- to_port = 22 -> null
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
terraform plan を実行すると、Terraformは「stateにないルールが追加されている」と検知し、次回の apply で手動追加したSSHルールを削除しようとする。
この plan 出力を確認せずに apply すると、担当者が追加したルールは削除される。一方、plan を確認することで「誰かが手動変更した」と気づくことができる。
drift を放置するとどうなるか
| 状態 | リスク |
|---|---|
| セキュリティグループに不要なルールが残る | 意図しないポート開放によるセキュリティリスク |
| インスタンスタイプをコンソールで変更 | Terraform apply で意図せずインスタンス再作成 |
| タグをコンソールで変更 | 課金配賦・モニタリングの誤作動 |
| RDSのパラメータグループをコンソールで変更 | apply時に意図しないRDS再起動 |
driftは「見えない変更」であるため、発見が遅れるほど被害が大きくなる。定期的な terraform plan 実行や、後続セクションで解説するdrift検知の自動化が重要だ。
2-6. まとめ: なぜstate管理・ロック・drift検知が必要なのか
ここまで4つの課題を見てきた。それぞれの原因と対策をまとめると次のとおりだ。
| 課題 | 根本原因 | 対策 |
|---|---|---|
| state競合 | 複数人が同時にstateを読み書きする | DynamoDBロックでアクセスを直列化 |
| apply競合 | ロックなしで古いstateを上書きする | DynamoDBロック + PR駆動apply(第2弾) |
| review不在 | main 直接 push + -auto-approve | PR必須 + plan 出力のレビュー(第2弾) |
| drift | コンソール手動変更がstateに反映されない | 定期 plan 実行 + IaC厳守ルール |
AWSとTerraform両方の視点での原則
- AWSコンソールはread-onlyで使う: 確認・モニタリングには使うが、変更はすべてTerraform経由で行う
- stateは共有リソース: 個人のローカルでstateを管理しない。S3バックエンドを全員が使う
- ロックは保険ではなく必須: 「小さなチームだから大丈夫」という運用は必ず破綻する
次のセクションからは、これらの課題を解消するS3+DynamoDBバックエンドの構築と、drift検知の実装を進めていく。
3. リモートbackend再入門(複数人視点)— S3+DynamoDB lock の役割と限界
3-1. 前作との関係(前提)
前作(ID:1208「Terraform実践 — モジュール化・tfstate・OIDC CI/CD」)で学んだ方は Section 3-2 のS3バケット作成から 読み進めてください。ローカルstateからリモートbackendへの移行手順はそちらで詳しく解説しています。
前作では「なぜリモートbackendが必要か」「移行コマンドの手順」を中心に解説しました。本記事では 複数人チームでそのbackendを運用したときに何が起きるか に焦点を当てます。
単独開発とチーム開発の違い
| 観点 | 単独開発(ローカルstate) | チーム開発(リモートbackend) |
|---|---|---|
| stateの所在 | 手元の terraform.tfstate | S3バケット(共有) |
| 同時実行の危険 | 自分しかいないので発生しない | 複数人が同時に apply する可能性がある |
| state破損の復旧 | ローカルで対処 | S3のバージョニングで過去状態に戻せる |
| アクセス制御 | 不要 | IAMポリシーで操作権限を制限する必要がある |
ローカルstateで運用していたチームがリモートbackendに移行したとき、最初に直面する問題は「同時 apply 競合」 です。AチームメンバーとBチームメンバーが同時に terraform apply を実行すると、state が上書きされ、どちらかの変更が消えるか、最悪の場合は両方の変更が混在した壊れたstateが残ります。
DynamoDB によるロック機能はこの問題を解決するために存在します。しかし 「ロックさえあれば安全」ではない ことも理解しておく必要があります。
3-2. S3 backend の設定(Terraform + AWSコンソール両方)
AWSコンソールでS3バケットを作成する
- AWS マネジメントコンソール → S3 を開く
- 「バケットを作成」 をクリック
- 以下の設定を入力する
| 項目 | 設定値 | 説明 |
|---|---|---|
| バケット名 | my-terraform-state-<アカウントID> | グローバルで一意な名前(アカウントIDを含めると一意になりやすい) |
| リージョン | ap-northeast-1(東京) | チームが使うリージョンに合わせる |
| オブジェクト所有者 | ACL無効(推奨) | デフォルトのまま |
| パブリックアクセスのブロック | すべてブロック | stateファイルを外部公開しないために必須 |
| バージョニング | 有効にする | state破損時に過去バージョンへ戻すために必須 |
| デフォルトの暗号化 | SSE-S3(または SSE-KMS) | stateには機密情報(パスワード等)が含まれることがある |
- 「バケットを作成」 をクリック
バージョニングを有効にすることで、terraform apply のたびに .tfstate ファイルの旧バージョンが保存されます。誤った変更を apply してしまった場合でも、S3コンソールから「バージョンを表示」→古いバージョンを「復元」できます。
AWSコンソールでDynamoDBテーブルを作成する
- AWS マネジメントコンソール → DynamoDB を開く
- 「テーブルの作成」 をクリック
- 以下の設定を入力する
| 項目 | 設定値 | 説明 |
|---|---|---|
| テーブル名 | terraform-state-lock | 任意の名前でよい |
| パーティションキー | LockID(文字列型) | Terraformが使う固定キー名(変えてはいけない) |
| テーブルクラス | DynamoDB 標準 | 低頻度アクセスでも標準で十分 |
| キャパシティモード | オンデマンド | lock操作は頻度が低いためオンデマンドがコスト効率よい |
- 「テーブルの作成」 をクリック
パーティションキーは必ず LockID(大文字のL・Iに注意) にしてください。Terraformのバックエンドコードがこのキー名をハードコードしているため、別の名前にするとロックが機能しません。
Terraform でバケットとテーブルを作成する
コンソール操作の代わりに Terraform で作成することもできます。ただし bootstrap 問題 があります。「stateを管理するためのS3バケット自体を Terraform で作る」と、そのS3バケットを管理するstateをどこに置くか問題が生じます。
実用的な解決策は2つあります:
– 方法A: S3バケットとDynamoDBテーブルだけ手動(コンソール/AWS CLI)で作り、残りのリソースをTerraformで管理する
– 方法B: bootstrap用の別Terraformプロジェクト(ローカルstate)でS3/DynamoDBを管理し、メインプロジェクトはリモートbackendを使う
以下は方法Bの bootstrap プロジェクト例です:
# bootstrap/main.tf
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# ここだけローカルstateを使う(bootstrapの宿命)
}
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-${var.account_id}"
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
}
resource "aws_dynamodb_table" "terraform_state_lock" {
name= "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
backend.tf の設定例
S3バケットとDynamoDBテーブルが作成できたら、メインプロジェクトに backend.tf を作成します:
# backend.tf
terraform {
backend "s3" {
bucket= "my-terraform-state-123456789012"
key= "production/terraform.tfstate"
region= "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
| パラメータ | 説明 |
|---|---|
bucket | stateを格納するS3バケット名 |
key | S3上のオブジェクトキー(パス)。環境ごとに変えること |
region | S3バケットのリージョン |
encrypt | 転送中の暗号化を有効にする(trueを常に指定) |
dynamodb_table | ロック用DynamoDBテーブル名 |
terraform init でbackendを切り替える
既存のプロジェクトでlocalbackendからS3 backendに切り替えるには、backend.tf を作成または変更した後に terraform init -migrate-state を実行します:
# S3 backendに切り替え(既存のローカルstateをS3に移行)
terraform init -migrate-state
実行すると以下のような確認が表示されます:
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "s3" backend. No existing state was found in the newly
configured "s3" backend. Do you want to copy this state to the new backend?
Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
yes を入力すると、ローカルの terraform.tfstate がS3にコピーされます。
コンソール画面での確認方法
- S3コンソール → 対象バケット →
production/terraform.tfstateオブジェクトが存在することを確認 - オブジェクトをクリック → 「バージョン」タブ で過去のstateが保存されていることを確認
- オブジェクトをダウンロードして内容確認(機密情報が含まれることがあるため取扱注意)
3-3. DynamoDBロックの仕組みと確認方法
ロック取得→apply→ロック解放の流れ
terraform apply を実行すると、Terraformは以下の順序で処理を行います:
1. DynamoDBにLockIDレコードを書き込む(ロック取得)
↓
2. S3からstateを読み込む
↓
3. planを実行(差分計算)
↓
4. ユーザーが「yes」を入力
↓
5. AWSリソースを変更する
↓
6. S3にstateを書き込む
↓
7. DynamoDBのLockIDレコードを削除(ロック解放)
ロックが取得されている間に別のメンバーが terraform apply を実行しようとすると:
╷
│ Error: Error acquiring the state lock
│
│ Error message: ConditionalCheckFailedException: The conditional request failed
│ Lock Info:
│ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
│Path:my-terraform-state-123456789012/production/terraform.tfstate
│Operation: OperationTypeApply
│Who: alice@example.com
│Version:1.6.0
│Created:2026-04-18 10:30:00.123456789 +0000 UTC
│Info:
╵
このエラーにより、同時実行による state 破損を防止 できます。
DynamoDBテーブルにロックレコードが作成される様子
terraform apply の実行中に DynamoDB コンソールで terraform-state-lock テーブルを開くと、以下のようなレコードが作成されています:
{
"LockID": {
"S": "my-terraform-state-123456789012/production/terraform.tfstate"
},
"Digest": {
"S": "8d777f385d3dfec8815d20f7496026dc"
},
"ID": {
"S": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
},
"Info": {
"S": "{\"ID\":\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\"Operation\":\"OperationTypeApply\",\"Info\":\"\",\"Who\":\"alice@example.com\",\"Version\":\"1.6.0\",\"Created\":\"2026-04-18T10:30:00.123456789Z\",\"Path\":\"my-terraform-state-123456789012/production/terraform.tfstate\"}"
},
"Operation": {
"S": "OperationTypeApply"
},
"Path": {
"S": "my-terraform-state-123456789012/production/terraform.tfstate"
},
"Version": {
"S": "1.6.0"
},
"Who": {
"S": "alice@example.com"
},
"Created": {
"S": "2026-04-18 10:30:00.123456789 +0000 UTC"
}
}
apply が正常完了するとこのレコードは自動的に削除されます。
ロックが残ったままになった場合の確認コマンド
ネットワーク切断・プロセスkill・CI/CD パイプラインの強制停止などにより apply が中断されると、ロックレコードが残ったままになります。
確認方法1: terraform コマンドで確認
# ロック状態を確認(apply を試みることで間接確認)
terraform plan
ロックが残っている場合は先述のエラーメッセージが出力され、ID フィールドにロックIDが表示されます。
確認方法2: AWS CLI で直接確認
# DynamoDBテーブルの全レコードをスキャン
aws dynamodb scan \
--table-name terraform-state-lock \
--region ap-northeast-1 \
--output json
出力例(ロックレコードが残っている場合):
{
"Items": [
{
"LockID": { "S": "my-terraform-state-123456789012/production/terraform.tfstate" },
"Who": { "S": "alice@example.com" },
"Created": { "S": "2026-04-18 10:30:00.123456789 +0000 UTC" },
"Operation": { "S": "OperationTypeApply" }
}
],
"Count": 1
}
正常時(ロックなし)は "Count": 0 になります。
確認方法3: AWSコンソールで確認
- DynamoDBコンソール →
terraform-state-lockテーブル - 「テーブルアイテムを探索」 をクリック
- アイテムが表示されていれば残留ロック →
Whoフィールドで誰が操作中だったか確認
3-4. force-unlock の使い所と注意
terraform force-unlock LOCK_ID の使い方
ロックが残ったままで plan や apply を実行できない場合、terraform force-unlock コマンドでロックを強制解除できます。
手順:
- まずロックIDを確認する
terraform plan
出力されるエラーメッセージの ID: フィールドのUUIDをメモします。
- force-unlock を実行する
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
確認プロンプトが表示されます:
Do you really want to force-unlock?
Terraform will remove the lock on the remote state.
This will allow local Terraform commands to modify this state, even though it
may be still be in use. Only 'yes' will be accepted to confirm.
Enter a value: yes
yesを入力してロック解除
Terraform state has been successfully unlocked!
AWS CLI での直接削除(非推奨・緊急時のみ)
# Terraform コマンドが使えない場合の最終手段
aws dynamodb delete-item \
--table-name terraform-state-lock \
--region ap-northeast-1 \
--key '{"LockID": {"S": "my-terraform-state-123456789012/production/terraform.tfstate"}}'
誤って使う危険性
force-unlock は 「本当にapplyが止まっている」と確認できた場合にのみ 使用してください。
最も危険なシナリオ: 別の apply 実行中に解除する
【状況】
Alice: terraform apply 実行中(時間がかかっている)
Bob:「Aliceのapplyが止まったかな」と勘違いしてforce-unlockを実行
【結果】
Aliceのapplyがロックなしで続行される
同時にBobもapplyを開始できる状態になる
→ stateが同時書き込みされ破損する可能性
force-unlock 実行前のチェックリスト
□ Slack/チャットで「今applyしている人はいるか」を全員に確認した
□ CI/CDパイプラインが実行中でないことを確認した(パイプライン画面を確認)
□ DynamoDBの "Created" 時刻が古く、明らかに中断されたと判断できる
□ "Who" フィールドに記載された本人に確認を取った(もしくは本人がいない)
□ S3のstateを確認し、中途半端な状態でないことを確認した
stateが中途半端な場合の対処
force-unlock後に terraform plan を実行して差分を確認します。実際のAWSリソースとstateに乖離がある場合(apply途中で止まった場合など)、terraform import や手動での terraform state 操作が必要になることがあります。
4. 環境分離戦略の比較 — workspace vs ディレクトリ分離 vs Terragrunt
4-1. なぜ環境分離が必要か
dev/stg/prod を同一stateで管理する危険性
複数の環境(開発・ステージング・本番)を 1つのstateファイル で管理することは技術的には可能ですが、実運用では重大なリスクがあります。
リソース誤削除のリスク
# 危険な例: 1つのmain.tfでdev/prod両方のリソースを管理
resource "aws_instance" "dev_app" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
resource "aws_instance" "prod_app" {
ami = "ami-0abcdef1234567890"
instance_type = "m5.large"
}
このような構成で terraform destroy を実行すると、dev と prod 両方のリソースが削除されます。-target フラグで対象を絞ることもできますが、指定ミスは本番削除に直結します。
同一stateで複数環境を管理する問題点まとめ
| リスク | 内容 |
|---|---|
| 誤削除 | terraform destroy が全環境のリソースを対象にする |
| state肥大化 | 全環境のリソースが1ファイルに集中し、planが遅くなる |
| 権限管理の困難 | devに触れるメンバーがprodのstateも見られる |
| 変数管理の複雑化 | dev/prod で異なる値を持つ変数の管理が煩雑になる |
| ロック競合の増加 | dev作業が本番のapplyをブロックする |
適切な環境分離の原則
1つの環境 = 1つのstateファイル
これにより、devのdestroyがprodに影響しない、環境ごとに異なるIAM権限を付与できる、などのメリットが得られます。
4-2. 手法1: Terraform workspace
workspace の基本操作
Terraform workspaceは、1つのTerraformプロジェクトから複数の独立したstateを管理する 仕組みです。
# workspaceの一覧表示
terraform workspace list
# 出力例:
# * default
#dev
#stg
#prod
# 新しいworkspaceを作成
terraform workspace new dev
# workspaceを切り替える
terraform workspace select dev
# 現在のworkspaceを確認
terraform workspace show
# 出力: dev
workspace とstateの関係(S3上のパス構造)
workspaceを使ってS3バックエンドに保存すると、stateファイルのパスが自動的に変わります:
S3バケット内のパス構造:
env:/
dev/
production/terraform.tfstate ← dev workspace
stg/
production/terraform.tfstate ← stg workspace
prod/
production/terraform.tfstate ← prod workspace
production/terraform.tfstate← default workspace
backend.tf の key に指定したパス(production/terraform.tfstate)が、workspace名のサブディレクトリ下に自動配置されます。
workspace 内での環境ごとの設定
terraform.workspace 変数を使って、workspace名に基づいた条件分岐を記述できます:
# variables.tf
variable "instance_type" {
default = "t3.micro"
}
locals {
env = terraform.workspace
# 環境ごとのインスタンスタイプ
instance_type_map = {
dev = "t3.micro"
stg = "t3.small"
prod = "m5.large"
}
instance_type = lookup(local.instance_type_map, local.env, var.instance_type)
}
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = local.instance_type
tags = {
Name = "app-${local.env}"
Environment = local.env
}
}
workspace のメリット・デメリット
| 観点 | 内容 |
|---|---|
| メリット | ディレクトリ構成がシンプルで済む |
| メリット | 既存プロジェクトへの導入が容易 |
| メリット | stateが自動的に分離される |
| デメリット | workspace切り替え忘れによる誤操作リスク |
| デメリット | モジュール構成が大きく変わる場合にstateの移行が困難 |
| デメリット | terraform.workspace の多用でコードが読みにくくなる |
| デメリット | 環境ごとに全く異なるリソース構成を管理しにくい |
workspace に向いているケース: 同じリソース構成で環境ごとにパラメータだけ変える小〜中規模プロジェクト
workspace に向いていないケース: devにはないprod専用リソース(WAF・Shield等)がある、環境ごとにモジュール構成が異なる
実際のディレクトリ構成とbackend設定例
workspace を使う場合、プロジェクト構成はシンプルになります:
my-infra/
├── backend.tf
├── main.tf
├── variables.tf
├── outputs.tf
└── modules/
├── vpc/
└── ec2/
# backend.tf(workspaceを使う場合、keyは1つでよい)
terraform {
backend "s3" {
bucket= "my-terraform-state-123456789012"
key= "my-infra/terraform.tfstate"
region= "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
# dev環境にapply
terraform workspace select dev
terraform apply -var-file="vars/dev.tfvars"
# prod環境にapply
terraform workspace select prod
terraform apply -var-file="vars/prod.tfvars"
4-3. 手法2: ディレクトリ分離(本シリーズ推奨)
ディレクトリ分離とは
各環境を 完全に独立したTerraformプロジェクト(ディレクトリ) として管理する方法です。本シリーズでは、明示性と安全性の観点からこの方法を推奨します。
my-infra/
├── environments/
│├── dev/
││├── main.tf
││├── backend.tf
││├── variables.tf
││└── terraform.tfvars
│├── stg/
││├── main.tf
││├── backend.tf
││├── variables.tf
││└── terraform.tfvars
│└── prod/
│ ├── main.tf
│ ├── backend.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── modules/
├── vpc/
│├── main.tf
│├── variables.tf
│└── outputs.tf
├── ec2/
│├── main.tf
│├── variables.tf
│└── outputs.tf
└── rds/
├── main.tf
├── variables.tf
└── outputs.tf
各環境のファイル構成例
各環境は独立したbackend設定を持ちます:
# environments/dev/backend.tf
terraform {
backend "s3" {
bucket= "my-terraform-state-123456789012"
key= "environments/dev/terraform.tfstate"
region= "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
# environments/prod/backend.tf
terraform {
backend "s3" {
bucket= "my-terraform-state-123456789012"
key= "environments/prod/terraform.tfstate"
region= "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
key のパスが環境ごとに異なるため、stateが完全に分離されます。
# environments/dev/main.tf
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
module "vpc" {
source = "../../modules/vpc"
vpc_cidr = var.vpc_cidr
env= "dev"
}
module "ec2" {
source = "../../modules/ec2"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
instance_type = var.instance_type
env = "dev"
}
# environments/dev/terraform.tfvars
aws_region = "ap-northeast-1"
vpc_cidr= "10.0.0.0/16"
instance_type = "t3.micro"
# environments/prod/terraform.tfvars
aws_region = "ap-northeast-1"
vpc_cidr= "10.1.0.0/16"
instance_type = "m5.large"
apply 操作
# dev環境にapply
cd environments/dev
terraform init
terraform apply
# prod環境にapply(別ディレクトリなので誤操作しにくい)
cd environments/prod
terraform init
terraform apply
作業ディレクトリを変えるだけで環境が切り替わるため、workspace切り替え忘れのリスクがありません。
各環境が独立したstateを持つ構成
S3バケット内のパス構造:
S3バケット内のパス構造(ディレクトリ分離の場合):
my-terraform-state-123456789012/
├── environments/
│├── dev/
││└── terraform.tfstate
│├── stg/
││└── terraform.tfstate
│└── prod/
│ └── terraform.tfstate
└── bootstrap/
└── terraform.tfstate
ディレクトリ分離のメリット・デメリット
| 観点 | 内容 |
|---|---|
| メリット | 環境間の完全な分離(devのdestroyがprodに絶対影響しない) |
| メリット | workspace切り替えミスによる事故がない |
| メリット | 環境ごとに異なるリソース構成(prod専用WAF等)を自然に表現できる |
| メリット | CI/CDパイプラインの設計がシンプル(ブランチとディレクトリを対応させやすい) |
| デメリット | 環境間でコードが重複する(DRY原則に反する) |
| デメリット | 設定変更を全環境に適用するときに1つずつ変更が必要 |
| デメリット | ファイル数が多くなる |
コードの重複はモジュール化で緩和できます。modules/ 配下に共通ロジックを切り出すことで、各環境の main.tf はモジュールの呼び出しと変数の差分だけになります。
4-4. 手法3: Terragrunt(紹介のみ)
Terragrunt とは
Terragrunt は、Terraform のラッパーツールです。Gruntwork社が開発するオープンソースプロジェクトで、ディレクトリ分離の冗長さ(コード重複)を解消する ことを主な目的としています。
DRY(Don’t Repeat Yourself)原則に基づき、各環境の backend.tf や provider.tf の重複を terragrunt.hcl に集約できます。
Terragrunt を使ったディレクトリ構成:
my-infra/
├── terragrunt.hcl ← ルートの共通設定(backend, provider)
├── environments/
│├── dev/
││└── terragrunt.hcl ← dev固有の変数のみ
│├── stg/
││└── terragrunt.hcl ← stg固有の変数のみ
│└── prod/
│ └── terragrunt.hcl ← prod固有の変数のみ
└── modules/
└── vpc/
# terragrunt.hcl(ルート)— backend設定を1箇所に集約
remote_state {
backend = "s3"
config = {
bucket= "my-terraform-state-123456789012"
key= "${path_relative_to_include()}/terraform.tfstate"
region= "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
# environments/dev/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
inputs = {
instance_type = "t3.micro"
vpc_cidr= "10.0.0.0/16"
}
各環境の terragrunt.hcl は変数の差分だけ記述すれば済み、backend.tf の重複がなくなります。
採用判断の目安
| 条件 | 判断 |
|---|---|
| チーム規模: 1〜3名、環境数: 2〜3 | ディレクトリ分離で十分。Terragruntは過剰 |
| チーム規模: 5名以上、環境数: 5以上 | Terragruntの導入コストが割に合う |
| モジュールを社内で多数管理している | Terragruntのモジュール参照機能が有効 |
| Terraform 初心者が多い | Terragruntはさらに学習コストが上がるため慎重に |
| Terraformの知識が成熟している | Terragruntへの移行は比較的スムーズ |
本シリーズではTerragruntは扱いませんが、チームの規模やリポジトリが拡大したタイミングで検討してください。
4-5. 比較まとめ表
| 手法 | 向き | メリット | デメリット | 推奨場面 |
|---|---|---|---|---|
| Terraform workspace | 小〜中規模 | 構成がシンプル、導入が容易 | 切り替えミスのリスク、複雑な構成差分に弱い | 同一構成で環境ごとにパラメータを変えるだけの場合 |
| ディレクトリ分離 | 中〜大規模 | 完全分離で安全、環境差異を自然に表現できる | コード重複(モジュール化で緩和可能) | チーム開発・CI/CD連携・本番環境を持つプロジェクト |
| Terragrunt | 大規模・複数プロジェクト | DRY原則でコード重複を解消 | 学習コスト、Terragrunt自体のバージョン管理が必要 | 環境数が多く、ディレクトリ分離の重複が問題になってきた場合 |
本シリーズの推奨: ディレクトリ分離
理由は3点あります。第一に、workspace切り替えミスという人的エラーの排除。第二に、CI/CDパイプラインとのブランチ戦略の対応(devブランチ→dev環境、mainブランチ→prod環境)が直感的。第三に、チームメンバーへのTerraformの学習コストが最小で済む(Terragruntの追加学習不要)。
コード重複のデメリットはモジュール化で解消でき、environments/ 配下の各ファイルは「モジュールを呼び出す薄いラッパー」として維持できます。
次のセクションでは、このディレクトリ分離構成を実際のCI/CDパイプライン(GitHub Actions + AWS)と組み合わせる方法を解説します。
5. state competition ハンズオン — 2人同時 apply を体験する
複数人でTerraformを運用するとき、最も避けたいのがstate の競合(state competition)だ。S3 リモートバックエンドと DynamoDB ロックを正しく設定していれば、2人が同時に terraform apply を試みても安全に排他制御される。このセクションでは、その動作を意図的に再現し、「ロックが機能する場合」「ロックを無効化した場合」「apply 中断後のロック残留」という3つのケースを通じて、ロック機能の価値を肌で理解する。
5-1. ハンズオン概要と準備
5-1-1. このハンズオンで体験すること
| ケース | 内容 | 結果 |
|---|---|---|
| ケース 1 | DynamoDB ロックが有効な状態で 2 つの apply を同時実行 | 後発の apply がロックエラーで弾かれる |
| ケース 2 | -lock=false でロックを無効化して同時実行 | 2 つの apply が競合し state が破損するリスクがある |
| ケース 3 | apply 中に Ctrl+C で強制終了 | ロックが残留し次回の apply がブロックされる |
5-1-2. 前提条件
このハンズオンは Section 3 で構築した S3 + DynamoDB バックエンドが使用可能であることを前提とする。まだ設定していない場合は先に Section 3 を完了させること。
必要なリソース(Section 3 で作成済みのもの):
| リソース | 用途 |
|---|---|
S3 バケット(例: my-tfstate-bucket) | state ファイルの保管 |
DynamoDB テーブル(例: terraform-lock-table)、パーティションキー LockID | ロック管理 |
| IAM ユーザー or ロール(必要な S3/DynamoDB 権限付き) | Terraform 実行権限 |
バックエンド設定が正しく機能しているかを確認する:
cd ~/terraform-team-handson
terraform init
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Successfully configured the backend "s3"! が表示されればバックエンドは正常に設定されている。
5-1-3. ハンズオン用サンプル Terraform コード
競合を再現するために、applyに数秒かかる最小構成のリソースを用意する。aws_s3_bucket はプロビジョニング自体が速いため、time_sleep リソースを使って意図的に apply 時間を延ばす。
main.tf:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
time = {
source = "hashicorp/time"
version = "~> 0.11"
}
}
backend "s3" {
bucket= "my-tfstate-bucket" # Section 3 で作成したバケット名に変更
key= "handson/competition.tfstate"
region= "ap-northeast-1"
dynamodb_table = "terraform-lock-table" # Section 3 で作成したテーブル名に変更
encrypt = true
}
}
provider "aws" {
region = "ap-northeast-1"
}
# apply に時間をかけるためのダミーリソース
resource "time_sleep" "wait" {
create_duration = "20s"
}
# ダミーバケット(名前は自分のアカウントに合わせて変更すること)
resource "aws_s3_bucket" "demo" {
bucket = "tf-competition-demo-${random_id.suffix.hex}"
depends_on = [time_sleep.wait]
}
resource "random_id" "suffix" {
byte_length = 4
}
terraform init でプロバイダを取得する:
terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Finding hashicorp/time versions matching "~> 0.11"...
- Finding hashicorp/random versions matching "~> 3.0"...
- Installed hashicorp/aws v5.x.x
- Installed hashicorp/time v0.11.x
- Installed hashicorp/random v3.x.x
Terraform has been successfully initialized!
5-1-4. ターミナルを 2 つ用意する
ケース 1 と 2 では、2 つのターミナルを同時に操作する。以下のいずれかの方法でペインを用意する。
方法A: ターミナルウィンドウを 2 つ開く
macOS Terminal.app や iTerm2 であれば、新しいウィンドウ(Cmd+N)を開き、両方のウィンドウで同じ作業ディレクトリに移動する:
cd ~/terraform-team-handson
方法B: tmux で 2 ペイン分割
tmux new-session -s competition
# 左右に分割
tmux split-window -h
# 左ペインに移動(Ctrl+b → ← キー)
# 右ペインに移動(Ctrl+b → → キー)
# 両ペインで作業ディレクトリに移動
cd ~/terraform-team-handson
以降の手順では「ターミナルA」「ターミナルB」と表記する。どちらのウィンドウ/ペインを A・B にするかは任意で構わない。
5-2. ケース 1: DynamoDB ロックが機能する場合
最も重要なケースだ。S3 + DynamoDB バックエンドが正しく設定されていれば、2 人が同時に apply を実行しても後発の apply はロックエラーで即座に弾かれる。
5-2-1. ターミナルA で apply を開始する
ターミナルA で apply を実行する。time_sleep リソースにより約 20 秒間 apply が継続する:
# ターミナルA
terraform apply -auto-approve
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_s3_bucket.demo will be created
+ resource "aws_s3_bucket" "demo" {
...
}
# random_id.suffix will be created
+ resource "random_id" "suffix" {
...
}
# time_sleep.wait will be created
+ time_sleep.wait will be created
...
Apply complete! ...(20秒後に完了)
apply が走り始めたら、完了を待たずにすぐにターミナルB に切り替える。
5-2-2. ターミナルB で apply を即座に実行する
ターミナルA の apply がまだ実行中の状態で、ターミナルB から同じ apply を実行する:
# ターミナルB
terraform apply -auto-approve
ロックが機能していれば、ターミナルB には以下のエラーが即座に表示される:
╷
│ Error: Error locking state: Error acquiring the state lock: ConditionalCheckFailedException: The conditional request failed
│
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.
│
│ Lock Info:
│ID: a3f2b1c4-d5e6-7890-abcd-ef1234567890
│Path:s3://my-tfstate-bucket/handson/competition.tfstate
│Operation: OperationTypeApply
│Who: alice@MacBook-Pro.local
│Version:1.8.x
│Created:2026-04-18 06:10:30.123456789 +0000 UTC
│Info:
╵
このエラーメッセージのポイント:
| フィールド | 意味 |
|---|---|
ID | DynamoDB の LockID に保存されているロック識別子(UUID) |
Path | ロックされている state ファイルのパス |
Operation | どの操作がロックを取得しているか(OperationTypeApply) |
Who | ロックを取得したユーザーとホスト名 |
Created | ロック取得時刻(UTC) |
ターミナルB は即座に終了する。ターミナルA の apply が完了するまでロックは保持され続ける。
5-2-3. DynamoDB テーブルでロックレコードを確認する
apply が実行中の間(ターミナルA が走っている状態)、DynamoDB テーブルにロックレコードが存在することを確認する。
AWS CLI で確認する場合:
aws dynamodb scan \
--table-name terraform-lock-table \
--region ap-northeast-1 \
--query "Items[*]" \
--output json
[
{
"LockID": {
"S": "my-tfstate-bucket/handson/competition.tfstate"
},
"Info": {
"S": "{\"ID\":\"a3f2b1c4-d5e6-7890-abcd-ef1234567890\",\"Operation\":\"OperationTypeApply\",\"Info\":\"\",\"Who\":\"alice@MacBook-Pro.local\",\"Version\":\"1.8.x\",\"Created\":\"2026-04-18T06:10:30.123456789Z\",\"Path\":\"s3://my-tfstate-bucket/handson/competition.tfstate\"}"
}
}
]
LockID が <バケット名>/<stateパス> の形式でレコードが存在していることを確認できる。
AWS マネジメントコンソールで確認する場合:
- DynamoDB コンソールを開く
- 左メニューから「テーブル」→
terraform-lock-tableを選択 - 「項目を探索」タブをクリック
- ロック実行中は
LockIDに state のパスが入ったレコードが表示される
5-2-4. apply 完了後のロック解放を確認する
ターミナルA の apply が完了すると(20秒後):
# ターミナルA
time_sleep.wait: Creating...
time_sleep.wait: Still creating... [10s elapsed]
time_sleep.wait: Still creating... [20s elapsed]
time_sleep.wait: Creation complete after 20s
aws_s3_bucket.demo: Creating...
aws_s3_bucket.demo: Creation complete after 2s [id=tf-competition-demo-a1b2c3d4]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
apply 完了後、再度 DynamoDB を確認するとロックレコードが消えている:
aws dynamodb scan \
--table-name terraform-lock-table \
--region ap-northeast-1 \
--query "Items[*]" \
--output json
[]
空配列が返り、ロックが解放されたことが確認できる。
5-3. ケース 2: DynamoDB ロックがない場合(ロック無効化テスト)
警告: このケースは本番環境では絶対に実行しないこと。state ファイルの破損や、インフラの意図しない変更・削除が発生するリスクがある。検証環境・ハンズオン用リソースのみで実施すること。
5-3-1. -lock=false フラグとは
-lock=false は Terraform の state ロックを完全に無効化するフラグだ。このフラグを使うと、DynamoDB のロックチェックが行われず、複数のプロセスが同時に state を読み書きできる状態になる。
ロックが無効な場合に発生しうる問題:
| 問題 | 説明 |
|---|---|
| state の上書き競合 | 2 つの apply がほぼ同時に完了した場合、先に書き込んだ state が後から上書きされる |
| リソースの二重作成 | state に記録される前に 2 つの apply が同じリソースを作成しようとする |
| ドリフトの発生 | state と実際のインフラが一致しなくなり、以降の plan/apply が正しく動作しなくなる |
5-3-2. -lock=false で同時 apply を実行する
まず、ケース 1 で作成したリソースを一旦削除してから実施する:
terraform destroy -auto-approve
aws_s3_bucket.demo: Destroying...
aws_s3_bucket.demo: Destruction complete after 1s
time_sleep.wait: Destroying...
time_sleep.wait: Destruction complete after 0s
Destroy complete! Resources: 3 destroyed.
ターミナルA と ターミナルB で、それぞれ -lock=false を付けて apply を同時に実行する。タイミングを合わせるために、両ターミナルでコマンドを入力した後、同時に Enter を押す:
# ターミナルA(先に入力して待機)
terraform apply -auto-approve -lock=false
# ターミナルB(ターミナルA と同タイミングで実行)
terraform apply -auto-approve -lock=false
両方が同時に走り出す:
# ターミナルA# ターミナルB
random_id.suffix: Creating... random_id.suffix: Creating...
random_id.suffix: Creation complete random_id.suffix: Creation complete
time_sleep.wait: Creating... time_sleep.wait: Creating...
time_sleep.wait: Still creating... time_sleep.wait: Still creating...
time_sleep.wait: Creation complete time_sleep.wait: Creation complete
aws_s3_bucket.demo: Creating... aws_s3_bucket.demo: Creating...
このとき、両方の apply が独立した random_id.suffix を生成するため、S3 バケット名が異なり 2 つのバケットが作成されることがある。あるいは、片方が失敗する場合もある。いずれの場合も state ファイルには最後に書き込んだプロセスの内容だけが残り、もう片方の変更は state に反映されない。
5-3-3. state の不整合を確認する
両方の apply が完了した後、state を確認する:
terraform state list
aws_s3_bucket.demo
random_id.suffix
time_sleep.wait
state には 1 セットのリソースしか記録されていない。しかし AWS コンソールでは 2 つのバケットが存在していることがある。この状態がドリフトだ。state と実際のインフラが一致していない。
ドリフトを解消するには terraform import や terraform refresh が必要になり、運用コストが大幅に増加する。DynamoDB ロックがいかに重要かを実感できるはずだ。
5-3-4. 後処理(作成されたリソースの削除)
# terraform state に記録されているリソースを削除
terraform destroy -auto-approve
state に記録されていないリソース(バケット等)が残っている場合は AWS CLI で手動削除する:
# 残留バケットを確認
aws s3 ls | grep tf-competition-demo
# バケットを空にして削除
aws s3 rb s3://<バケット名> --force
5-4. ケース 3: apply 中断後のロック残留
apply 実行中に Ctrl+C で強制終了すると、DynamoDB のロックレコードが残留することがある。ロックが残留したままでは次回の apply がブロックされる。
5-4-1. apply を途中で強制終了する
クリーンな状態から開始する(前のケースで destroy 済みであることを確認する):
terraform state list
No state file was found!
apply を開始し、time_sleep が実行中の間に Ctrl+C を押す:
terraform apply -auto-approve
random_id.suffix: Creating...
random_id.suffix: Creation complete after 0s [id=e5f6a7b8]
time_sleep.wait: Creating...
time_sleep.wait: Still creating... [10s elapsed]
^C
╷
│ Error: operation canceled
╵
There are 3 resource(s) in your configuration. 1 have been successfully created,
and 0 have been modified. There is 1 resource(s) that could not be created.
Terraform does not guarantee that it will successfully rollback the created resources.
Ctrl+C(^C)を押すと apply が中断される。このとき、Terraform は DynamoDB のロックレコードを削除できない場合がある。
5-4-2. ロックが残留していることを確認する
aws dynamodb scan \
--table-name terraform-lock-table \
--region ap-northeast-1 \
--query "Items[*]" \
--output json
[
{
"LockID": {
"S": "my-tfstate-bucket/handson/competition.tfstate"
},
"Info": {
"S": "{\"ID\":\"b4c5d6e7-f8a9-0123-bcde-f456789012ab\",\"Operation\":\"OperationTypeApply\",\"Info\":\"\",\"Who\":\"alice@MacBook-Pro.local\",\"Version\":\"1.8.x\",\"Created\":\"2026-04-18T06:25:10.987654321Z\",\"Path\":\"s3://my-tfstate-bucket/handson/competition.tfstate\"}"
}
}
]
ロックレコードが残留している。この状態で terraform apply を実行すると:
terraform apply -auto-approve
╷
│ Error: Error locking state: Error acquiring the state lock: ConditionalCheckFailedException: The conditional request failed
│
│ Lock Info:
│ID: b4c5d6e7-f8a9-0123-bcde-f456789012ab
│Path:s3://my-tfstate-bucket/handson/competition.tfstate
│Operation: OperationTypeApply
│Who: alice@MacBook-Pro.local
│Version:1.8.x
│Created:2026-04-18 06:25:10.987654321 +0000 UTC
│Info:
╵
apply がブロックされる。
5-4-3. terraform force-unlock でロックを解除する
リスク説明:
terraform force-unlockは、本当にそのロックが孤立した(= ロックを取得したプロセスがすでに終了している)ことを確認してから実行すること。別のユーザーが正当に apply を実行中の状態でこのコマンドを実行すると、同時書き込みを許可してしまい state が破損するリスクがある。チームで使用する場合は必ずコミュニケーションを取ってから実行すること。
ロック ID をエラーメッセージから取得し、force-unlock を実行する:
terraform force-unlock b4c5d6e7-f8a9-0123-bcde-f456789012ab
Do you really want to force-unlock?
Terraform will remove the lock on the remote state.
This will allow local Terraform commands to modify this state, even though it
may be still be in use. Only 'yes' will be accepted to confirm.
Enter a value: yes
Terraform state has been successfully unlocked!
The state has been unlocked, and Terraform commands should now be able to
run successfully. Please verify that no other Terraform process is
currently attempting to use this state, and that the local state and remote
state are the same.
yes を入力してロックを解除する。解除後、DynamoDB のロックレコードが削除されたことを確認する:
aws dynamodb scan \
--table-name terraform-lock-table \
--region ap-northeast-1 \
--query "Items[*]" \
--output json
[]
5-4-4. DynamoDB コンソールからロックレコードを手動削除する方法
force-unlock コマンドが使えない状況(例: Terraform バイナリがない環境など)では、AWS マネジメントコンソールから直接 DynamoDB のレコードを削除することも可能だ。
- DynamoDB コンソールを開く
- 左メニューから「テーブル」→
terraform-lock-tableを選択 - 「項目を探索」タブをクリック
- ロックレコードを選択(
LockIDが state パスになっているレコード) - 「アクション」→「項目を削除」をクリック
- 確認ダイアログで「削除」をクリック
注意: コンソールからの手動削除は Terraform を完全にバイパスする操作だ。本当にロックが孤立していることを確認してから実施すること。Terraform の
force-unlockコマンドが使える場合はそちらを優先する。
5-4-5. ロック解除後に apply を再実行する
ロック解除後、apply を再実行する前に state の状態を確認する:
terraform state list
random_id.suffix
中断前に random_id.suffix だけが作成済みの状態だ。apply を再実行すると残りのリソースが作成される:
terraform apply -auto-approve
time_sleep.wait: Creating...
time_sleep.wait: Still creating... [10s elapsed]
time_sleep.wait: Still creating... [20s elapsed]
time_sleep.wait: Creation complete after 20s
aws_s3_bucket.demo: Creating...
aws_s3_bucket.demo: Creation complete after 2s [id=tf-competition-demo-e5f6a7b8]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
正常に完了する。
5-5. ベストプラクティス: plan/apply の実行制御
ここまでのハンズオンで、DynamoDB ロックがチームの同時 apply から state を守る最後の防衛線であることを体感した。ただし、ロックはあくまで競合を「防ぐ」仕組みであり、「誰がいつ apply してよいか」という運用ルールを補完するものではない。このセクションでは、ロックと合わせて使うことで apply をより安全にする実践的なプラクティスを紹介する。
5-5-1. Makefile による apply 前確認ゲート
make apply のようなラッパーを作ることで、apply 実行前に必ず plan の確認を挟む習慣を強制できる。
Makefile:
.PHONY: init plan apply destroy
TFPLAN := tfplan.out
init:
terraform init
plan:
terraform plan -out=$(TFPLAN)
@echo "---"
@echo "プランを確認し、問題がなければ 'make apply' を実行してください。"
apply: $(TFPLAN)
@echo "以下のプランを適用します:"
terraform show -no-color $(TFPLAN)
@echo "---"
@read -p "本当に apply を実行しますか? [yes/no]: " confirm && \
[ "$$confirm" = "yes" ] && terraform apply $(TFPLAN) || echo "apply をキャンセルしました。"
rm -f $(TFPLAN)
$(TFPLAN):
@echo "先に 'make plan' を実行してプランファイルを生成してください。"
@exit 1
destroy:
@read -p "本当に destroy を実行しますか? [yes/no]: " confirm && \
[ "$$confirm" = "yes" ] && terraform destroy -auto-approve || echo "destroy をキャンセルしました。"
使い方:
# 1. プランを生成
make plan
# 出力を確認した後で apply
make apply
make apply は $(TFPLAN) ファイルが存在しない場合に失敗するため、terraform plan をスキップした apply を防止できる。また、apply 直前に再度プランを表示して確認を求めるため、意図しない変更を実行するリスクが低減される。
5-5-2. terraform plan -out=tfplan による事前確認フロー
-out オプションでプランをバイナリファイルとして保存すると、plan 時点のプランを apply 時に正確に再現できる。プランファイルなしの apply では、plan と apply の間にリソースの状態が変わっていた場合に意図しない差分が生じることがある。
# ステップ 1: プランを生成してファイルに保存
terraform plan -out=tfplan.out
Terraform will perform the following actions:
# aws_s3_bucket.demo will be created
+ resource "aws_s3_bucket" "demo" {
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Saved the plan to: tfplan.out
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan.out"
# ステップ 2: プランファイルの内容を人間が読める形式で確認
terraform show tfplan.out
# ステップ 3: 確認済みのプランファイルを使って apply
terraform apply tfplan.out
time_sleep.wait: Creating...
time_sleep.wait: Creation complete after 20s
aws_s3_bucket.demo: Creating...
aws_s3_bucket.demo: Creation complete after 2s
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
プランファイルを使った apply では、plan と apply の間に別の変更があってもプランファイルの内容だけが適用される。意図しない追加変更の適用を防ぐ効果がある。
プランファイルにはアクセスキーなどの機密情報が含まれる場合がある。
.gitignoreに*.outを追加してバージョン管理に含めないようにすること。
.gitignore の設定例:
# Terraform
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.backup
*.tfplan
*.out
tfplan.out
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
5-5-3. PR レビュー後のみ apply を許可するフロー(次弾への橋渡し)
ここまでのプラクティスはローカル環境での実行制御だ。チームで安全に Terraform を運用するためには、CI/CD パイプラインに apply を組み込み、PR レビューをゲートにする構成が最も効果的だ。
代表的なフローを示す:
開発者A がコードを変更
↓
feature ブランチを push
↓
Pull Request を作成
↓
CI が自動で terraform plan を実行 → プラン差分を PR コメントに投稿
↓
チームメンバーがプランを確認してレビュー・承認
↓
main ブランチへマージ
↓
CI が terraform apply を自動実行
このフローの利点:
| 利点 | 説明 |
|---|---|
| apply 前にチームがプランを確認できる | 意図しないリソース変更・削除を PR レビューで防止 |
| apply は CI が実行するため個人差がない | ローカル環境の差異による問題を排除 |
| apply の実行履歴が CI ログに残る | 誰がいつどの変更を適用したか追跡可能 |
| main へのマージ = apply の承認 | レビュープロセスと apply 実行が自然に連動する |
具体的な実装(GitHub Actions + Terraform Cloud / Atlantis / tfcmt など)は第 2 弾で詳しく解説する。このセクションでは概念と目指すべきフローを理解しておこう。
まとめ
このセクションで体験した内容を振り返る:
| ケース | 結果 | 学び |
|---|---|---|
| ケース 1 (ロック有効・同時 apply) | 後発の apply がエラーで弾かれた | DynamoDB ロックが state 競合を確実に防ぐ |
| ケース 2 (-lock=false・同時 apply) | state が不整合になるリスクを確認 | ロック無効化は本番では絶対に禁止 |
| ケース 3 (apply 中断・ロック残留) | 次回 apply がブロックされた | force-unlock は孤立確認後のみ実行 |
S3 + DynamoDB バックエンドの組み合わせが機能することを、エラーメッセージや DynamoDB レコードを通じて実際に確認できた。また、ロックはあくまでも最終防衛線であり、Makefile や plan ファイルによる実行制御、PR レビューフローを組み合わせることで、より安全な運用が実現できる。
第 2 弾では、このバックエンド構成を土台に、GitHub Actions を使った CI/CD パイプラインへの Terraform 統合を解説する。
6. drift検知の運用 — 定期 terraform plan + GitHub Actions + Slack通知
Terraformでインフラをコード管理していても、AWSコンソールから誰かが手動変更を加えた瞬間に「コードと実態のズレ」が生まれる。このズレを drift(ドリフト) と呼ぶ。driftを放置すると、次の terraform apply 時に意図しない変更や削除が実行される危険がある。本セクションでは、driftを自動検出してSlackに通知するGitHub Actionsワークフローを構築する。
6-1. driftとは何か
driftの定義
Terraformが管理するインフラの「あるべき状態(.tf ファイル)」と「実際のAWS上の状態」が乖離することをdriftという。Terraformは .tfstate ファイルにリソースの状態を記録しているが、AWSコンソールや他のツールで直接変更を加えると .tfstate の記録と実態がずれる。
.tf ファイル(コード)
↓ terraform apply
.tfstate(管理記録) ←── ここが実態と乖離する
↓
AWS上の実リソース ←── コンソール手動変更でここだけ変わる
driftが発生しやすい典型例
| シナリオ | 結果 |
|---|---|
| 開発者がコンソールからEC2のセキュリティグループにルールを手動追加 | 次の terraform apply で削除される |
| RDSのパラメータグループをコンソールから変更 | Terraformが「差分あり」と判定して上書き |
| S3バケットのバージョニングをコンソールから有効化 | .tf に記述がなければ無効に戻される |
| IAMロールにポリシーを手動アタッチ | Terraform管理外のポリシーとして扱われ削除対象になる可能性 |
具体例: EC2のSecurityGroup手動追加 → terraform apply で削除
1. Terraform管理のEC2に対し、コンソールからポート8080の許可ルールを手動追加
2. この時点でdriftが発生(.tfstateには8080の記録なし)
3. 別の開発者が terraform apply を実行
4. Terraformは「8080のルールは管理外(driftによる追加)」と判断
5. 8080のルールが削除される → 本番サービスに影響が出る可能性
このような事故を防ぐには、driftを定期的に自動検出し、チームに通知する仕組みが必須である。
6-2. drift検知の仕組み: terraform plan の活用
terraform plan でdriftを確認する
terraform plan はコードと実際のインフラの差分を表示するコマンドである。driftが発生していれば plan の出力に差分が表示される。
terraform plan
出力例(driftがある場合):
Terraform will perform the following actions:
# aws_security_group_rule.allow_http will be destroyed
- resource "aws_security_group_rule" "allow_http" {
- cidr_blocks = ["0.0.0.0/0"]
- from_port= 8080
- protocol = "tcp"
- to_port = 8080
- type = "ingress"
}
Plan: 0 to add, 0 to change, 1 to destroy.
-detailed-exitcode オプション
自動化スクリプトでdriftの有無をプログラム的に判断するには -detailed-exitcode オプションを使う。
| 終了コード | 意味 |
|---|---|
0 | 差分なし(driftなし) |
1 | エラー(認証失敗・構文エラー等) |
2 | 差分あり(driftあり) |
terraform plan -detailed-exitcode -out=tfplan
EXIT_CODE=$?
if [ $EXIT_CODE -eq 2 ]; then
echo "Drift detected!"
elif [ $EXIT_CODE -eq 1 ]; then
echo "Error occurred during plan"
else
echo "No drift detected"
fi
-out=tfplan オプションでplan結果をファイルに保存しておくと、後から terraform show tfplan で詳細を確認できる。
plan出力をテキストに変換する
Slackへの通知やログ保存のために、plan出力を人が読めるテキスト形式に変換するには terraform show を使う。
# planファイルをテキストに変換
terraform show -no-color tfplan > plan_output.txt
-no-color オプションでANSIエスケープコードを除去し、Slackやログファイルで読みやすくする。
6-3. 定期drift検知のGitHub Actionsワークフロー
毎日定刻に terraform plan を実行し、driftを検出したらSlackに通知する完全なGitHub Actionsワークフローを構築する。
ワークフローファイルの配置
.github/
└── workflows/
└── drift-detection.yml
完全版ワークフロー: .github/workflows/drift-detection.yml
name: Drift Detection
on:
schedule:
- cron: '0 0 * * *' # 毎日 09:00 JST(UTC 00:00)
workflow_dispatch:# 手動実行も可能
jobs:
drift-check:
name: Terraform Drift Check
runs-on: ubuntu-latest
permissions:
id-token: write# OIDC認証に必要
contents: read
env:
TF_VERSION: '1.7.5'
AWS_REGION: 'ap-northeast-1'
WORKING_DIR: './terraform'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
working-directory: ${{ env.WORKING_DIR }}
run: |
terraform init \
-backend-config="bucket=${{ secrets.TF_STATE_BUCKET }}" \
-backend-config="key=terraform.tfstate" \
-backend-config="region=${{ env.AWS_REGION }}" \
-backend-config="dynamodb_table=${{ secrets.TF_LOCK_TABLE }}" \
-input=false
- name: Terraform Plan (drift detection)
id: plan
working-directory: ${{ env.WORKING_DIR }}
run: |
set +e
terraform plan \
-detailed-exitcode \
-no-color \
-out=tfplan \
-input=false \
2>&1 | tee plan_output.txt
EXIT_CODE=${PIPESTATUS[0]}
echo "exit_code=${EXIT_CODE}" >> $GITHUB_OUTPUT
echo "Plan exit code: ${EXIT_CODE}"
set -e
- name: Convert plan to readable format
if: steps.plan.outputs.exit_code == '2'
working-directory: ${{ env.WORKING_DIR }}
run: |
terraform show -no-color tfplan > plan_readable.txt
- name: Upload plan artifact
if: steps.plan.outputs.exit_code == '2'
uses: actions/upload-artifact@v4
with:
name: drift-plan-${{ github.run_id }}
path: |
${{ env.WORKING_DIR }}/tfplan
${{ env.WORKING_DIR }}/plan_readable.txt
retention-days: 30
- name: Notify Slack (drift detected)
if: steps.plan.outputs.exit_code == '2'
run: |
PLAN_SUMMARY=$(head -50 ${{ env.WORKING_DIR }}/plan_readable.txt || echo "(詳細はArtifactを参照)")
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
PAYLOAD=$(cat <<EOF
{
"text": "🚨 *Terraform Drift Detected!*",
"attachments": [
{
"color": "#ff0000",
"fields": [
{
"title": "Repository",
"value": "${{ github.repository }}",
"short": true
},
{
"title": "Triggered at",
"value": "$(date -u '+%Y-%m-%d %H:%M UTC')",
"short": true
},
{
"title": "Workflow Run",
"value": "<${RUN_URL}|Click here to view details>",
"short": false
},
{
"title": "Plan Summary (first 50 lines)",
"value": "\`\`\`${PLAN_SUMMARY}\`\`\`",
"short": false
}
],
"footer": "Terraform Drift Detection | Action required: investigate and fix"
}
]
}
EOF
)
curl -s -X POST \
-H 'Content-type: application/json' \
--data "${PAYLOAD}" \
"${{ secrets.SLACK_WEBHOOK_URL }}"
- name: Notify Slack (plan error)
if: steps.plan.outputs.exit_code == '1'
run: |
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
curl -s -X POST \
-H 'Content-type: application/json' \
--data "{
\"text\": \"⚠️ *Terraform Plan Error in Drift Detection*\",
\"attachments\": [{
\"color\": \"#ff9900\",
\"fields\": [{
\"title\": \"Workflow Run\",
\"value\": \"<${RUN_URL}|Click here to view details>\",
\"short\": false
}]
}]
}" \
"${{ secrets.SLACK_WEBHOOK_URL }}"
- name: Notify Slack (no drift)
if: steps.plan.outputs.exit_code == '0'
run: |
curl -s -X POST \
-H 'Content-type: application/json' \
--data "{
\"text\": \"✅ *No Terraform Drift Detected* — Infrastructure is in sync with code ($(date -u '+%Y-%m-%d %H:%M UTC'))\"
}" \
"${{ secrets.SLACK_WEBHOOK_URL }}"
- name: Fail job if drift detected
if: steps.plan.outputs.exit_code == '2'
run: |
echo "Drift was detected. Please investigate and resolve."
exit 1
ワークフローのポイント解説
| 設定項目 | 説明 |
|---|---|
schedule: cron: '0 0 * * *' | UTC 00:00 = JST 09:00 に毎日実行 |
workflow_dispatch | GitHubのUI/APIから手動実行可能 |
id-token: write | OIDC認証でSecretなしにAWSへアクセス |
set +e / PIPESTATUS | terraform planの終了コードをteeと共存させて取得 |
exit 1(最後のステップ) | drift検知時にワークフローをFailed扱いにし、GitHubのUI上で視認性を上げる |
6-4. Slack通知の設定
Slack Incoming Webhook URLの取得
- Slackワークスペースにブラウザからアクセスし、 「その他の管理機能」→「アプリを管理する」 を開く
- 検索ボックスで「Incoming WebHooks」を検索し、アプリを追加する
- 「新しい設定を追加する」 をクリック
- 通知先のチャンネル(例:
#terraform-alerts)を選択して 「Incoming Webhookインテグレーションの追加」 をクリック - 表示される Webhook URL をコピーする
Webhook URLの形式:
https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX
GitHub Repository SecretsへのWebhook URLの登録
GitHubリポジトリの Settings → Secrets and variables → Actions → New repository secret から登録する。
| Secret名 | 値 |
|---|---|
SLACK_WEBHOOK_URL | コピーしたIncoming Webhook URL |
AWS_ROLE_ARN | OIDCで使用するIAMロールのARN |
TF_STATE_BUCKET | Terraform状態ファイルを保存するS3バケット名 |
TF_LOCK_TABLE | ロック管理用DynamoDBテーブル名 |
AWSコンソールでの登録手順:
1. GitHubリポジトリのページを開く
2. Settings タブをクリック
3. 左サイドバーの Secrets and variables → Actions を選択
4. New repository secret ボタンをクリック
5. Name欄に SLACK_WEBHOOK_URL を入力し、Secret欄にWebhook URLを貼り付けて Add secret をクリック
plan出力をSlackメッセージに含める方法
ワークフロー内で plan_readable.txt の先頭50行をSlackメッセージに含めている。Slackのメッセージ文字数制限(約3,000文字)を超えないように先頭行数を調整すること。
# 先頭50行を取得
PLAN_SUMMARY=$(head -50 plan_readable.txt)
# 文字数が多い場合は切り詰め
PLAN_SUMMARY=$(head -50 plan_readable.txt | head -c 2000)
Slackメッセージのフォーマット例
driftが検出された場合の通知:
🚨 Terraform Drift Detected!
Repository: myorg/infrastructure
Triggered at: 2026-04-18 00:00 UTC
Workflow Run: [Click here to view details]
Plan Summary (first 50 lines):
# aws_security_group_rule.manual_rule will be destroyed
- resource "aws_security_group_rule" "manual_rule" {
- from_port = 8080
- to_port= 8080
...
}
Plan: 0 to add, 0 to change, 1 to destroy.
Action required: investigate and fix
driftがない場合の通知:
✅ No Terraform Drift Detected — Infrastructure is in sync with code (2026-04-18 00:00 UTC)
6-5. AWSコンソールでの確認
GitHub ActionsのWorkflow実行結果の確認
- GitHubリポジトリの Actions タブを開く
- 左サイドバーで Drift Detection ワークフローを選択
- 実行履歴の一覧が表示される。各実行の結果は以下のアイコンで確認できる:
- ✅ 緑チェック: 正常完了(driftなし)
- ❌ 赤×: Failedまたはdrift検出
⚠️ 黄△: ワークフロー自体のエラー
実行行をクリックしてジョブの詳細ログを表示し、 Terraform Plan ステップを展開して差分の詳細を確認する
plan出力のArtifact保存
ワークフロー内の actions/upload-artifact ステップにより、drift検出時のplanファイルは自動的にArtifactとして30日間保存される。
- name: Upload plan artifact
if: steps.plan.outputs.exit_code == '2'
uses: actions/upload-artifact@v4
with:
name: drift-plan-${{ github.run_id }}
path: |
${{ env.WORKING_DIR }}/tfplan
${{ env.WORKING_DIR }}/plan_readable.txt
retention-days: 30
Artifactのダウンロード手順:
1. ワークフロー実行詳細ページを開く
2. ページ下部の Artifacts セクションで drift-plan-<run_id> をクリック
3. ZIPファイルとしてダウンロードし、plan_readable.txt を開いて差分を確認する
CloudTrailで誰がコンソール操作したかを追跡する方法
driftの原因となったコンソール操作者を特定するには、AWS CloudTrailを使用する。
AWSコンソールでの手順:
1. CloudTrail サービスを開く
2. 左サイドバーの イベント履歴 を選択
3. 以下のフィルタを設定して絞り込む:
| フィルタ | 設定例 |
|---|---|
| 時間範囲 | drift検出時刻の前後24時間 |
| イベント名 | AuthorizeSecurityGroupIngress(SG手動追加の場合) |
| ユーザー名 | 特定のIAMユーザーを絞り込む場合 |
| リソース名 | 対象のセキュリティグループID |
- 該当イベントをクリックすると イベントの詳細 に
userIdentityが表示される:
{
"eventVersion": "1.08",
"userIdentity": {
"type": "IAMUser",
"userName": "taro.yamada",
"arn": "arn:aws:iam::123456789012:user/taro.yamada"
},
"eventName": "AuthorizeSecurityGroupIngress",
"eventTime": "2026-04-18T08:30:00Z",
"requestParameters": {
"groupId": "sg-0123456789abcdef0",
"ipPermissions": {
"items": [
{
"ipProtocol": "tcp",
"fromPort": 8080,
"toPort": 8080
}
]
}
}
}
主要なCloudTrailイベント名とdriftの対応
| 手動操作 | CloudTrailイベント名 |
|---|---|
| セキュリティグループルール追加 | AuthorizeSecurityGroupIngress / AuthorizeSecurityGroupEgress |
| EC2インスタンスタグ変更 | CreateTags / DeleteTags |
| S3バケット設定変更 | PutBucketVersioning / PutBucketPolicy |
| IAMポリシーアタッチ | AttachRolePolicy / AttachUserPolicy |
| RDSパラメータグループ変更 | ModifyDBParameterGroup |
6-6. drift検知のベストプラクティス
定期実行 + PR時にも実行
driftの検知頻度を高めるため、定期実行に加えてPullRequest時にも terraform plan を実行する。
on:
schedule:
- cron: '0 0 * * *' # 毎日09:00 JST
pull_request:
paths:
- 'terraform/**'
workflow_dispatch:
PR時は差分コメントとして残すことで、コードレビュー時にインフラの変更意図を確認できる。
drift発見時のオペレーション手順
1. Slack通知を受け取る
2. GitHub ActionsのWorkflow実行詳細を確認し、plan_readable.txtで差分を把握する
3. CloudTrailで誰がいつどのような操作をしたかを調査する
4. 差分の対応方針を決定する:
A. コンソール変更を「正」とする場合 → terraform import でstateに取り込む
B. コードを「正」とする場合 → terraform apply でインフラをコードの状態に戻す
C. コンソール変更を恒久反映する場合 → .tfファイルに変更を加えてPR → apply
drift対応フローチャート
driftを検知
│
▼
意図的な変更か?
│
├─ YES → .tf ファイルに反映して PR
│→ import または plan/apply で同期
│
└─ NO → 誰が変更したかCloudTrailで調査
→ 変更を元に戻す or インポートして管理下に置く
→ 再発防止: SCPやIAMポリシーでコンソール直接操作を制限
terraform import でdriftを解消するケース
コンソールで作成・変更したリソースをTerraformの管理下に取り込む場合は terraform import を使う。
# 書式
terraform import <リソースタイプ>.<リソース名> <AWSリソースID>
# 例: コンソールで手動追加したセキュリティグループルールを管理下に
terraform import aws_security_group_rule.allow_8080 sg-0123456789abcdef0_ingress_tcp_8080_8080_0.0.0.0/0
# 例: コンソールで作成したS3バケットを管理下に
terraform import aws_s3_bucket.logs my-log-bucket-name
terraform import でstateにリソースを取り込んだ後は、対応する .tf ファイルのリソース定義も追加・修正する必要がある。
# import 後に .tf ファイルにも追加
resource "aws_security_group_rule" "allow_8080" {
type = "ingress"
from_port= 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.main.id
}
その後 terraform plan を実行して差分がゼロになったことを確認してからPRを作成する。
ガードレールの設置: SCP によるコンソール直接操作の制限
driftの根本原因はコンソール直接操作にある。AWS Organizations の Service Control Policy(SCP) を使って、Terraform管理外の操作を制限するのが最も効果的な予防策である。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDirectConsoleModification",
"Effect": "Deny",
"Action": [
"ec2:AuthorizeSecurityGroupIngress",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress",
"ec2:RevokeSecurityGroupEgress"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::*:role/TerraformExecutionRole",
"arn:aws:iam::*:role/GitHubActionsRole"
]
}
}
}
]
}
このSCPにより、TerraformのIAMロールとGitHub ActionsのIAMロール以外からのSG変更操作を拒否できる。
まとめ: drift管理のチェックリスト
- [ ] drift検知GHAワークフローを設置し、毎日定期実行している
- [ ] Slack Incoming WebhookをRepository Secretsに登録した
- [ ] drift検出時のplan出力がArtifactとして保存されている
- [ ] CloudTrailでコンソール操作者を追跡できる体制がある
- [ ] drift発見時のオペレーション手順がチームに周知されている
- [ ] 必要に応じてSCPでコンソール直接操作を制限している
- [ ]
terraform importの使い方をチームメンバーが理解している
7. state破損からの復旧 — 強制unlock・state pull/push・rm/import
チームでTerraformを運用していると、避けられない事故が起きることがあります。terraform apply の途中でネットワークが切れた、誰かがコンソールからリソースを手動削除した、最悪のケースではS3バケットごとstateファイルが消えた——こうした状況に直面したとき、冷静に対処できるかどうかがプロダクションの安定性を左右します。
このセクションでは、state破損の代表的なシナリオを整理し、各状況で使うべきコマンドと手順を実践形式で解説します。「壊れてから調べる」ではなく、事前に手順を把握しておくことが最大のリスク軽減策です。
7-1. state破損のシナリオ
state破損には大きく3つのパターンがあります。それぞれ原因と症状が異なるため、まず状況を正確に診断することが重要です。
シナリオA: apply中断・ネットワーク断によるstateの不整合
terraform apply の実行中にプロセスが強制終了した場合、AWSリソースは中途半端な状態で作成されているのに、stateはそのリソースを認識していない、あるいは逆にstateにはレコードがあるのにリソースが存在しない、という不整合が生じます。
症状の例:
Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Path:s3://my-tfstate-bucket/prod/terraform.tfstate
Operation: OperationTypeApply
Who: user@hostname
Version:1.7.0
Created:2024-03-15 09:23:11.123456789 +0000 UTC
Info:
このエラーは、前回のapplyが異常終了してDynamoDBのロックレコードが残存しているときに発生します。別のapplyが本当に実行中なのか、ゾンビロックなのかを区別することが最初のステップです。
シナリオB: 複数メンバーが同一リソースをTerraform管理外で削除
チームの誰かがAWSコンソールやCLIでリソースを手動削除すると、stateにはそのリソースが「存在する」と記録されているのに、実際にはAWS上に存在しないという「ドリフト」が発生します。
症状の例:
│ Error: error reading S3 Bucket (my-app-bucket): NoSuchBucket: The specified bucket does not exist
│with aws_s3_bucket.app,
│on main.tf line 12, in resource "aws_s3_bucket" "app":
│12: resource "aws_s3_bucket" "app" {
次のterraform plan実行時にエラーが出るか、該当リソースの再作成が提案されます。チームでのTerraform運用において、コンソール操作禁止ルールの徹底がいかに重要かを示す典型例です。
シナリオC: S3バケットの誤削除でstateファイルが消えた
最も深刻なケースです。tfstateを格納しているS3バケット自体を誰かが削除してしまうと、すべてのリソースの管理情報が失われます。バージョニングが有効であれば復元できますが、バケットごと削除された場合は手動でstateを再構築する必要があります。
症状の例:
│ Error: Failed to get existing workspaces: S3 bucket does not exist.
│
│The referenced S3 bucket must have been previously created. If the S3 bucket
│was created within the last minute, please wait for a minute or two and try
│again.
このエラーを見たら、まずS3コンソールで対象バケットの存在確認と、削除保護・バージョニング設定の状況を確認してください。
7-2. 緊急対応: force-unlock
ロックが残存している場合の対処手順を説明します。必ず「本当に別のapplyが実行中でないか」を確認してから実行してください。 force-unlockを誤って実行すると、実行中のapplyと競合してstateが破損する可能性があります。
ロックIDの確認方法
方法1: エラーメッセージから取得
前述のエラーメッセージの ID: フィールドに記載されているUUIDがロックIDです。
方法2: DynamoDBコンソールで確認
- AWSコンソール → DynamoDB → テーブル →
terraform-state-lock(または設定したテーブル名) - 「項目を探索」をクリック
- ロックレコードが存在する場合、
LockID列にs3://バケット名/パスの形式で表示されます
方法3: AWS CLIで確認
aws dynamodb scan \
--table-name terraform-state-lock \
--query "Items[*].{LockID:LockID.S,Info:Info.S}" \
--output table
出力例:
---------------------------------------------------------
| Scan |
+------------------------+------------------------------+
| Info | LockID |
+------------------------+------------------------------+
| {"ID":"xxxxxxxx-...} | s3://my-tfstate-bucket/... |
+------------------------+------------------------------+
Info カラムの JSON を確認し、Who フィールドで誰がロックを取得したかを確認します。そのメンバーに「applyが今も実行中か」を確認してから次のステップに進んでください。
force-unlockの実行
ゾンビロックであることを確認したら、以下のコマンドで強制解除します:
terraform force-unlock LOCK_ID
実際のコマンド例:
terraform force-unlock xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
確認プロンプトが表示されます:
Do you really want to force-unlock?
Terraform will remove the lock on the remote state.
This will allow local Terraform commands to modify this state, even though it
may be still be in use. Only 'yes' will be accepted to confirm.
Enter a value: yes
yes を入力すると、DynamoDBのロックレコードが削除されます。
解除後の確認:
aws dynamodb scan \
--table-name terraform-state-lock \
--select COUNT
{
"Count": 0,
"ScannedCount": 0,
"ResponseMetadata": { ... }
}
Count: 0 を確認してから、terraform plan を実行して現状を把握してください。
7-3. state操作コマンドの使い方
Terraformには、stateファイルを直接操作するためのサブコマンド群があります。これらは「壊れたときの修復ツール」であると同時に、「リソースの所有権をTerraformに移管・返還するツール」でもあります。
terraform state pull — stateをローカルにダウンロード
# stateファイルをローカルにダウンロード(バックアップ)
terraform state pull > backup-$(date +%Y%m%d).tfstate
用途:
– 作業前のバックアップ取得
– stateの現状確認(JSONとして読める)
– 手動編集が必要な場面(後述の state push と組み合わせ)
実行前の確認事項:
– terraform init が完了しており、backendへの接続が確立されていること
– 出力ファイルの保存先に書き込み権限があること
取得したstateファイルの中身を確認するには:
cat backup-$(date +%Y%m%d).tfstate | jq '.resources[].type' | sort | uniq
"aws_dynamodb_table"
"aws_iam_role"
"aws_s3_bucket"
"aws_s3_bucket_versioning"
管理対象リソースの種別一覧が確認できます。
terraform state push — stateをリモートにアップロード
# stateファイルをリモートにアップロード(復旧)
terraform state push backup-20240101.tfstate
用途:
– バックアップからの復旧
– state pull → 手動編集 → state push による修復
実行前の確認事項:
– アップロード先のstateバージョンが現在より古いことを確認(新しいstateを古いもので上書きしない)
– チームメンバーが同時に作業していないことを確認
– terraform plan でアップロード後の差分を把握してから apply を実行する
警告:
state pushは強力なコマンドです。誤ったstateをpushすると、それ以降のplan/applyがそのstateを正として動作します。作業前のstateを必ず別名でバックアップし、作業記録をチームのSlackやNotionに残してください。
terraform state rm — stateから特定リソースを削除
# stateから特定リソースを削除(Terraform管理から外す)
terraform state rm aws_s3_bucket.example
用途:
– 手動削除済みのリソースをstateから取り除く(次の plan でエラーが出なくなる)
– リソースをTerraform管理から外して別の管理方法に移行する
実行前の確認事項:
– 削除対象のリソース名を terraform state list で正確に確認する
– そのリソースが本当にAWS上に存在しないか、または意図的に管理対象から外すのかを確認する
まず管理対象リソースの一覧を確認:
terraform state list
aws_dynamodb_table.terraform_locks
aws_s3_bucket.app
aws_s3_bucket.tfstate
aws_s3_bucket_versioning.tfstate_versioning
特定リソースの詳細確認:
terraform state show aws_s3_bucket.app
# aws_s3_bucket.app:
resource "aws_s3_bucket" "app" {
bucket = "my-app-bucket-prod"
id= "my-app-bucket-prod"
...
}
確認後に削除:
terraform state rm aws_s3_bucket.app
Removed aws_s3_bucket.app
Successfully removed 1 resource instance(s).
terraform import — 既存リソースをstateに取り込む
# 既存リソースをstateに取り込む
terraform import aws_s3_bucket.example my-existing-bucket
用途:
– コンソールで手動作成したリソースをTerraform管理下に置く
– state rm でTerraform管理から外したリソースを再度取り込む
– 別のTerraformプロジェクトで管理していたリソースを移管する
実行手順:
- まずTerraformコード(
.tfファイル)に対応するresourceブロックを記述する
resource "aws_s3_bucket" "example" {
bucket = "my-existing-bucket"
}
terraform importを実行する
terraform import aws_s3_bucket.example my-existing-bucket
aws_s3_bucket.example: Importing from ID "my-existing-bucket"...
aws_s3_bucket.example: Import prepared!
Prepared aws_s3_bucket for import
aws_s3_bucket.example: Refreshing state... [id=my-existing-bucket]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
terraform planを実行して差分を確認する
terraform plan
コードと実際のリソース状態に差がある場合、ここで差分が表示されます。コードを実際の状態に合わせるか、apply で実際のリソースをコードの定義に合わせるかを判断してください。
importアドレスの調べ方:
各リソースタイプのimportアドレス形式は、Terraform公式ドキュメントの各リソースページの「Import」セクションに記載されています。主要リソースの例:
| リソースタイプ | importアドレス形式 | 例 |
|---|---|---|
aws_s3_bucket | バケット名 | my-bucket |
aws_dynamodb_table | テーブル名 | my-table |
aws_vpc | VPC ID | vpc-12345678 |
aws_iam_role | ロール名 | my-role |
aws_security_group | セキュリティグループID | sg-12345678 |
7-4. S3バージョニングを使ったstate復元
Section 2でS3バケットにバージョニングを設定しました。この設定が、stateファイルの事故復旧に直接役立ちます。
AWSコンソールでのバージョン確認・ダウンロード
- S3コンソール → 対象バケット(例:
my-tfstate-bucket)を開く - 左ペインの「バケットを表示」で
terraform.tfstateファイルを選択 - 「バージョン」タブをクリック
- バージョン一覧が表示される(最新から古い順)
バージョンID 最終更新日時 サイズ
--------------------------------------------------------------------
abc123def456ghi789 2024-03-15 09:30:0045.2 KB ← 最新(破損している可能性)
xyz987uvw654rst321 2024-03-15 09:00:0044.8 KB ← 復元候補
mno111pqr222stu333 2024-03-14 18:00:0044.7 KB ← さらに古いバージョン
- 復元したいバージョンを選択し、「ダウンロード」をクリック
- ダウンロードしたファイルを
restore-YYYYMMDD.tfstateのような名前で保存
AWS CLIでのバージョン確認・ダウンロード
# バージョン一覧の確認
aws s3api list-object-versions \
--bucket my-tfstate-bucket \
--prefix prod/terraform.tfstate \
--query "Versions[*].{VersionId:VersionId,LastModified:LastModified,Size:Size}" \
--output table
---------------------------------------------------------
| ListObjectVersions |
+------------------+------------------------------+------+
|LastModified| VersionId | Size |
+------------------+------------------------------+------+
| 2024-03-15T... | abc123def456ghi789... | 46310|
| 2024-03-15T... | xyz987uvw654rst321... | 45875|
+------------------+------------------------------+------+
# 特定バージョンをダウンロード
aws s3api get-object \
--bucket my-tfstate-bucket \
--key prod/terraform.tfstate \
--version-id xyz987uvw654rst321 \
restore-20240315.tfstate
復元後の整合性確認
# 復元したstateをリモートに適用
terraform state push restore-20240315.tfstate
# 現状との差分を確認
terraform plan
terraform plan の出力で差分が表示される場合、それはstateを復元した時点からの変更(または削除)を示しています。各差分について:
# aws_xxx.yyy will be created→ stateには記録がないがAWS上にリソースがある(terraform importが必要)# aws_xxx.yyy will be destroyed→ stateには記録があるがAWS上にリソースがない(terraform state rmが必要)# aws_xxx.yyy will be updated→ 設定値の差分(コードまたはstateを修正)
重要: 復元後にいきなり terraform apply を実行しないでください。plan の出力を精査し、チームで影響範囲を確認してから apply を実行します。
7-5. 破損を防ぐためのバックアップ設計
事後対応より事前予防が重要です。以下の設計をTerraformプロジェクト開始時に組み込んでおくことで、state破損のリスクを大幅に低減できます。
S3バージョニングの有効化(必須)
Section 2で設定済みですが、改めて強調します。バージョニングなしのS3 backendは運用すべきでありません。 設定は以下の通りです:
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
バージョニングを有効にすると、terraform apply のたびにstateの新バージョンが自動保存されます。
lifecycle policyで古いバージョンを自動削除
バージョニングを有効にし続けると、時間の経過とともに古いバージョンが蓄積してストレージコストが増大します。lifecycle policyで自動削除のルールを設定してください:
resource "aws_s3_bucket_lifecycle_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
id = "tfstate-version-cleanup"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 90
newer_noncurrent_versions = 10
}
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
}
このルールの意味:
– noncurrent_days = 90: 最新バージョン以外のバージョンは90日後に削除
– newer_noncurrent_versions = 10: 常に最新10バージョンを保持(90日以内でも)
– abort_incomplete_multipart_upload: 中断したアップロードは7日後に自動削除
重要環境のstateファイルの定期エクスポート
本番環境のstateは、GitHub Actionsのスケジュールワークフローで定期的にエクスポートしておくと安心です:
name: Backup Terraform State
on:
schedule:
- cron: '0 1 * * *' # 毎日午前1時(UTC)
workflow_dispatch:
jobs:
backup:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- name: Backup state file
run: |
DATE=$(date +%Y%m%d)
aws s3 cp \
s3://my-tfstate-bucket/prod/terraform.tfstate \
s3://my-tfstate-backup-bucket/prod/terraform-${DATE}.tfstate
このワークフローは毎朝1時にstateファイルを別バケットに日次バックアップします。バックアップ専用バケットにはバージョニングと削除保護を有効化し、本番バケットとは別のIAMポリシーで保護することをお勧めします。
まとめ: state保護の3層防御
| 層 | 対策 | 効果 |
|---|---|---|
| 第1層 | S3バージョニング有効化 | apply単位でのロールバックが可能 |
| 第2層 | DynamoDBロック | 同時書き込みによる競合を防止 |
| 第3層 | 別バケットへの日次バックアップ | バケット誤削除からの復旧が可能 |
3層すべてを設定することで、ほぼすべてのstate破損シナリオに対応できます。
8. まとめと次のステップ
8-1. この記事のまとめ
この記事では、複数人でTerraformを安全に運用するための基盤として、以下の内容を実践しました:
- S3 remote backend の構築: tfstateをローカルからクラウドに移行し、チームで共有できる状態を実現した
- DynamoDBによるstate locking: 複数人が同時にapplyを実行しても、stateが競合しない排他制御の仕組みを導入した
- ワークスペース分離:
terraform workspaceを使って、同一コードベースからdev/staging/prodの環境を分離する方法を習得した - ディレクトリ分離戦略: workspaceと比較して、ディレクトリ分離がどのような場面で有効かを理解した
- drift検知: GitHub Actions のスケジュールワークフローで毎日
terraform planを実行し、コンソール操作による設定ズレを自動検出する仕組みを構築した - state操作コマンド:
state pull/push/rm/importの使い方と、適切な使用場面を理解した - state破損からの復旧: force-unlock、S3バージョニングを使ったロールバック、手動importによる再構築の手順を把握した
チーム開発Terraformの「最低限やること」チェックリスト
新しいTerraformプロジェクトをチームで開始する際、以下の項目をすべて確認してください:
□ S3 backend 設定済み(バージョニング有効、MFA削除保護有効)
□ DynamoDB lock テーブル設定済み(PAY_PER_REQUEST)
□ 環境分離(workspace or ディレクトリ)設計済み・合意済み
□ drift検知workflow(GHA schedule: terraform plan)設定済み
□ state操作コマンドをチーム全員が理解済み(勉強会推奨)
□ state破損時の連絡フロー・対応手順をドキュメント化済み
□ tfstateバケットのバックアップ設計済み(lifecycle policy + 日次コピー)
このチェックリストをプロジェクト開始時のオンボーディングドキュメントに組み込んでおくことで、後から「ロックを設定し忘れていた」「バージョニングが無効だった」というヒヤリハットを防げます。
8-2. 次弾予告と末尾CTA
state管理の基盤が整った。次回はこれをGitHub PRフローと連携させ、チームで安全にapplyを実行するCI/CDパイプラインを構築する。
具体的には、PRを作成したときに自動で terraform plan を実行してレビュワーに差分を見せ、マージ後に terraform apply を自動実行するワークフローを、GitHub Actions + OIDC(secretlessな認証)を使って実装します。「誰がいつapplyを承認したか」のトレーサビリティも確保しながら、ヒューマンエラーを排除する自動化の仕組みを構築します。
次の記事 → 第2弾: PR駆動TerraformCI/CD — GitHub Actions+OIDCで複数人レビューフローを構築
8-3. 参考リンク
Terraform公式ドキュメント
- Backend Configuration — S3 — S3 backendの全設定オプション
- State — Locking — stateロックの仕組みと設定方法
- Command: state — state サブコマンドのリファレンス
- Command: force-unlock — force-unlockの詳細と注意事項
- Command: import — importコマンドのリファレンス
- Workspaces — workspaceの概念と使い方
HashiCorp Learn / Terraform Tutorials
- Store Remote State — AWS S3 remote backendのセットアップチュートリアル
- Manage Resources in Terraform State — state CLI操作の実践チュートリアル
- Import Terraform Configuration — importの実践チュートリアル
前作シリーズ(Git/GitHub × Terraform 実践シリーズ)