1. 概要
この記事では、以下の内容を解説します。
lifecycleブロックの4つのオプション(create_before_destroy/prevent_destroy/ignore_changes/replace_triggered_by)- 本番リソースの誤削除を防ぐ
prevent_destroyの正しい使い方 - ダウンタイムを最小化する
create_before_destroyの仕組み - Terraform外部の変更を無視する
ignore_changesのユースケース precondition/postconditionによるカスタムバリデーション(Terraform 1.2以降)- よくある落とし穴と対処法
lifecycleブロックはTerraformがリソースをどう作成・更新・削除するかを細かく制御するための仕組みです。本番環境を安全に運用するうえで、prevent_destroyとignore_changesは特に重要です。
2. lifecycleブロックとは
lifecycleブロックはすべてのresourceブロック内に書けるメタ引数です。resourceの動作(ライフサイクル)をカスタマイズします。
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.medium"
lifecycle {
create_before_destroy = true # 置換時に先に新規作成
prevent_destroy = true # 削除を防ぐ
ignore_changes = [tags] # tagsの変更を無視
}
}
4つのオプションは独立して使え、組み合わせることもできます。
| オプション | 効果 | 主なユースケース |
|---|---|---|
create_before_destroy | 置換時に先に新規作成、後から旧リソースを削除 | ダウンタイム最小化 |
prevent_destroy | terraform destroyやapplyによる削除を禁止 | 本番リソースの誤削除防止 |
ignore_changes | 指定した属性の変更差分を無視 | 外部ツールが変更する属性の保護 |
replace_triggered_by | 別リソースの変更でこのリソースを強制置換 | 依存関係の明示的な置換制御 |
⚠️ 重要な制約(リテラル値のみ使用可能):
lifecycleブロックの設定値にはリテラル値のみ使用できます。変数や式は使用できません。これはlifecycleの処理がTerraformの依存グラフ構築フェーズで行われるため、通常の式評価より前に処理される必要があるためです。
# ❌ エラー: lifecycleブロック内で変数は使えない
lifecycle {
prevent_destroy = var.is_production # エラー
}
# ✅ 正しい: リテラル値のみ
lifecycle {
prevent_destroy = true
}
⚠️ 注意:
lifecycleブロックはresourceブロック内にのみ記述できます。moduleブロック自体にlifecycleを書くことはできません。モジュール内の個別リソースに設定する必要があります。
3. create_before_destroy
通常の置換の流れとダウンタイム
Terraformは通常、リソースを削除してから再作成します(destroy then create)。この順序ではリソースが存在しない時間が生じ、サービスのダウンタイムになります。
通常の置換フロー(ダウンタイムあり):
1. 旧リソースを削除 ← この間サービス停止
2. 新リソースを作成
create_before_destroyで先に新規作成してから削除
create_before_destroy = trueにすると、順序が逆になります。
create_before_destroy の置換フロー(ダウンタイムなし):
1. 新リソースを作成 ← 旧リソースはまだ動いている
2. 旧リソースを削除
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
# AMIをdata sourceで動的取得
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# セキュリティグループにcreate_before_destroyを設定
# (ルール変更時にEC2が一時的にSGなしになることを防ぐ)
resource "aws_security_group" "web" {
name_prefix = "web-sg-"
vpc_id = aws_vpc.main.id
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"]
}
lifecycle {
create_before_destroy = true # SG更新時に先に新SGを作成してから旧SGを削除
}
tags = {
Name = "web-sg"
Environment = "production"
ManagedBy = "terraform"
}
}
💡 ポイント:
name_prefixを使うと、再作成のたびにユニークな名前が生成されるため、名前の衝突を避けられます。name(固定値)を使うと旧リソースが削除される前に名前が重複してエラーになります。
注意点(depends_on との組み合わせ)
create_before_destroy = trueのリソースに依存する他のリソースも、自動的にcreate_before_destroy = trueの扱いになります。依存チェーン全体で置換順序が連鎖することを意識してください。
4. prevent_destroy
使い方とエラーメッセージ
prevent_destroy = trueを設定すると、そのリソースを削除しようとしたときにエラーが発生します。
resource "aws_db_instance" "main" {
identifier = "production-db"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.medium"
allocated_storage = 100
db_name = "appdb"
username = "admin"
password = var.db_password
# 本番DBの誤削除を防ぐ
lifecycle {
prevent_destroy = true
}
tags = {
Name = "production-db"
Environment = "production"
ManagedBy = "terraform"
}
}
削除しようとすると以下のエラーが出ます。
│ Error: Instance cannot be destroyed
│
│ on main.tf line 12:
│ 12: resource "aws_db_instance" "main" {
│
│ Resource aws_db_instance.main has lifecycle.prevent_destroy set, but the
│ plan calls for this resource to be destroyed. To avoid this error and
│ continue with the plan, either disable lifecycle.prevent_destroy or reduce
│ the scope of the plan using the -target option.
terraform destroyを完全に防ぐ方法ではない
prevent_destroy = trueはlifecycleブロックがコードに存在する間だけ有効です。コードからprevent_destroy = trueを削除してterraform applyすれば削除できます。
prevent_destroyで防げるもの:
✅ terraform apply による意図しない削除
✅ for_each / count の要素削除による間接的な削除
✅ リソース設定変更による置換(-/+)
prevent_destroyで防げないもの:
❌ コードから lifecycle ブロックを削除してからの terraform apply
❌ terraform state rm してからの手動削除
⚠️ 本番DBや重要なS3バケットには設定を推奨しますが、「絶対に削除できない」わけではありません。 チームへの周知と合わせて運用してください。
5. ignore_changes
特定属性の変更を無視する
Terraform外部(手動操作・AWS AutoScaling・外部スクリプト等)でリソースの属性が変更された場合、次回terraform planで差分として検出されます。ignore_changesを使うと、指定した属性の変更を無視してdiffが出なくなります。
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.medium"
lifecycle {
# AMIは定期更新ジョブが変更するため、Terraformは追跡しない
# instance_typeはAWS側でスケールアップされる可能性があるため無視
ignore_changes = [
ami,
instance_type,
tags["LastDeployedAt"], # デプロイスクリプトが更新するタグは無視
]
}
tags = {
Name = "web"
Environment = "production"
ManagedBy = "terraform"
}
}
ignore_changes = all の使い方
すべての属性変更を無視するにはallを指定します。Terraformはリソースの存在のみを管理し、設定の差分はすべて無視します。
resource "aws_ssm_parameter" "config" {
name = "/app/config"
type = "SecureString"
value = "initial-value" # 初回作成時の値
lifecycle {
# アプリが動的に値を変更するため、Terraformは初回作成以降は関与しない
ignore_changes = all
}
tags = {
Name = "app-config"
ManagedBy = "terraform"
}
}
sensitive属性とignore_changesの組み合わせ
sensitive = trueの変数を使った属性(DBパスワード等)が毎回差分として出る場合、ignore_changesで対処できます。
resource "aws_db_instance" "main" {
password = var.db_password # sensitive変数
lifecycle {
# パスワードはTerraform管理外で更新されることがあるため無視
ignore_changes = [password]
}
}
ignore_changesが「効かない」と感じるケース
ignore_changesを設定していても差分が出続けるケースがあります。
ケース1: 属性名が間違っている
# ❌ 間違い: tagsではなくtag(単数形)と書いてしまう
lifecycle {
ignore_changes = [tag] # エラーにはならないが効かない
}
# ✅ 正しい
lifecycle {
ignore_changes = [tags]
}
ケース2: ネストした属性の指定が不正確
# ✅ ネストした属性はドット記法で指定する
lifecycle {
ignore_changes = [
tags["Environment"], # 特定のタグキーのみ無視
]
}
ケース3: Terraformがリソースを再作成(置換)している場合
ignore_changesは更新(update in-place)の差分を無視しますが、リソースの置換(-/+)が発生する場合は効きません。置換を制御するにはcreate_before_destroyやreplace_triggered_byを使います。
6. replace_triggered_by(Terraform 1.2以降)
別リソースや別リソースの属性が変更されたとき、このリソースを強制的に置換(再作成)させます。
resource "aws_launch_template" "web" {
name_prefix = "web-"
image_id = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.medium"
tags = {
Name = "web-launch-template"
ManagedBy = "terraform"
}
}
resource "aws_autoscaling_group" "web" {
name = "web-asg"
min_size = 2
max_size = 10
desired_capacity = 2
vpc_zone_identifier = aws_subnet.private[*].id
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
lifecycle {
# launch_templateが更新されたらASGも強制置換してローリング更新を発動する
replace_triggered_by = [
aws_launch_template.web
]
}
tag {
key = "Name"
value = "web-asg"
propagate_at_launch = true
}
}
7. precondition / postcondition(Terraform 1.2以降)
lifecycleブロック内にprecondition(適用前チェック)とpostcondition(適用後チェック)を書けます。
variable "environment" {
type = string
}
resource "aws_db_instance" "main" {
identifier = "${var.environment}-db"
engine = "mysql"
engine_version = "8.0"
instance_class = var.environment == "prd" ? "db.t3.medium" : "db.t3.micro"
allocated_storage = 20
db_name = "appdb"
username = "admin"
password = var.db_password
# 本番環境ではMulti-AZを必須とするprecondition
multi_az = var.environment == "prd"
lifecycle {
precondition {
condition = var.environment != "prd" || var.multi_az == true
error_message = "本番環境(prd)ではmulti_az = trueが必須です。"
}
postcondition {
condition = self.endpoint != ""
error_message = "RDSのエンドポイントが取得できませんでした。"
}
}
tags = {
Name = "${var.environment}-db"
Environment = var.environment
ManagedBy = "terraform"
}
}
precondition / postcondition はリソース単位の条件検証に使います。変数単位の検証(validationブロック)や、リソース外の状態確認(checkブロック)との使い分けは専用記事を参照してください。
→ precondition / postcondition — ライフサイクル中の条件検証
8. よくあるエラーと落とし穴
prevent_destroyがある状態でリソースを削除しようとした
│ Error: Instance cannot be destroyed
解決方法: lifecycleブロックからprevent_destroy = trueを削除(またはコメントアウト)してからterraform applyする。削除後は元に戻すことを忘れずに。
create_before_destroyで名前の衝突エラーが発生する
│ Error: creating Security Group: InvalidGroup.Duplicate
原因: name(固定値)を指定していると、旧リソースが削除される前に同名の新リソースを作ろうとしてエラーになる。
解決方法: nameの代わりにname_prefixを使う。
# ❌ nameを使うと衝突する
resource "aws_security_group" "web" {
name = "web-sg"
lifecycle { create_before_destroy = true }
}
# ✅ name_prefixを使う
resource "aws_security_group" "web" {
name_prefix = "web-sg-"
lifecycle { create_before_destroy = true }
}
ignore_changesを設定したのに毎回差分が出る
原因1: 属性名の誤り(スペルミス・単複形の違い)
原因2: ignore_changesで指定した属性以外が変更されている
原因3: Terraformのバグまたはプロバイダーの仕様変更
確認方法: terraform plan -json | jq '.resource_changes[].change'で具体的に変更されている属性名を確認する。
replace_triggered_by が Terraform 1.1以前で使えない
│ Error: Unsupported argument
│ replace_triggered_by is not expected here.
解決方法: Terraform 1.2以降にアップグレードする。またはreplace_triggered_byを使わずにterraform apply -replace=<リソースアドレス>で手動置換する。
9. 関連記事
- resourceブロックの使い方 — lifecycleが設定できるリソースブロックの基本
- depends_on の使い方 — 明示的な依存関係の設定
- state管理とは — prevent_destroyを迂回するstate rm操作
- Terraformよくあるエラー集 — lifecycle関連エラーの一覧
- count — 同じリソースを複数作成する
- for_each — map/setでリソースを動的に作成
10. まとめ
lifecycleブロックはリソースの作成・更新・削除の挙動をカスタマイズするcreate_before_destroy = true→ 置換時に先に新規作成してダウンタイムを最小化(セキュリティグループ等に有効)prevent_destroy = true→ 誤削除を防ぐが、コードから削除すれば迂回できる。本番DBや重要リソースに設定推奨ignore_changes = [attr]→ Terraform外部で変更される属性(タグ・AMI・AutoScalingが変更するフィールド等)の差分を無視するreplace_triggered_by→ 別リソースの変更でこのリソースを強制置換する(Terraform 1.2以降)name_prefixとcreate_before_destroyはセットで使う(名前衝突を防ぐため)
動作確認バージョン: Terraform >= 1.9 / AWS Provider ~> 5.0 対象リージョン: ap-northeast-1(東京) 公式ドキュメント: https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle