lifecycleブロック — リソースの削除・更新・無視を制御する

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_destroyignore_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_destroyterraform destroyapplyによる削除を禁止本番リソースの誤削除防止
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 = truelifecycleブロックがコードに存在する間だけ有効です。コードから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_destroyreplace_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. 関連記事


10. まとめ

  • lifecycleブロックはリソースの作成・更新・削除の挙動をカスタマイズする
  • create_before_destroy = true → 置換時に先に新規作成してダウンタイムを最小化(セキュリティグループ等に有効)
  • prevent_destroy = true → 誤削除を防ぐが、コードから削除すれば迂回できる。本番DBや重要リソースに設定推奨
  • ignore_changes = [attr] → Terraform外部で変更される属性(タグ・AMI・AutoScalingが変更するフィールド等)の差分を無視する
  • replace_triggered_by → 別リソースの変更でこのリソースを強制置換する(Terraform 1.2以降)
  • name_prefixcreate_before_destroyはセットで使う(名前衝突を防ぐため)

動作確認バージョン: Terraform >= 1.9 / AWS Provider ~> 5.0 対象リージョン: ap-northeast-1(東京) 公式ドキュメント: https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle