Terraform

Terraform lifecycle.replace_triggered_by: Safe Forced Replacement on Specific Changes (Design & Exam Prep)

2026-04-19
NicheeLab Editorial Team

Added in Terraform 1.2, lifecycle.replace_triggered_by is a meta-argument that forces a resource to be explicitly replaced (recreated) when another resource or module output changes. Normally updates and replacements are decided by attribute diffs, but this argument is valuable for settings that are only read at creation time or whenever you need to lock in the safety of an external dependency.

This article covers when to use replace_triggered_by correctly and where it goes wrong, how it compares to other features, the angles the exam tends to probe, and the verification patterns you'll use in practice. We assume you're already familiar with the Terraform language and the basics of the lifecycle block.

Fundamentals of replace_triggered_by and How It Compares

When the value of the references you list (a whole resource, a specific attribute, or a module output) changes, replace_triggered_by makes Terraform plan a replacement instead of an update. That lets you intentionally recreate a resource even when the provider supports in-place updates.

Typical examples are "components that only read external settings on first creation" and "resources like certificates or images that must definitely be swapped out when the underlying hash changes." Pointing at a whole resource means "any diff in that resource triggers replacement," while pointing at an attribute means "replacement runs only when that attribute's value changes."

  • Targets: resource references, resource attribute references, and module-output references — anything whose value change Terraform can detect at plan time.
  • Granularity: evaluated per resource instance (per element under count/for_each).
  • Effect: regardless of other diffs, a change in a trigger reference is planned as "must be replaced."
  • Caveat: dependency ordering is the job of depends_on. replace_triggered_by expresses the intent to replace, not creation order.
FeaturePrimary PurposeTrigger / How to SpecifyEffect on Plan
lifecycle.replace_triggered_byForce replacement on a specific external changeResource / attribute / module-output referenceAny change in the referenced value plans "must be replaced"
depends_onControl create/destroy orderResource / module referenceDoes not affect diffs; only modifies the dependency graph
null_resource.triggersRecreate a null_resource when a value changesAny value (string or map)Recreates the null_resource only; does not apply to other resources
lifecycle.ignore_changesIgnore diffs on specific attributesAttribute names (set or list)Suppresses diffs so no update/replace is planned (other factors can still trigger replacement)

The dependency-and-replacement flow of replace_triggered_by

module.caoutput: bundle_hashresource.kubernetes_secret.apilifecycle { replace_triggered_by = [module.ca...] }Plan marks it "must be replaced"Replacement (create new → destroy old)

Basic pattern: force replacement from a module-output or attribute change

# Module (example): outputs the certificate bundle hash
module "ca" {
  source = "./modules/ca"  # any implementation
  # ...
}

# Example 1: always recreate the Secret on module-output change
resource "kubernetes_secret" "api" {
  metadata {
    name      = "api-secret"
    namespace = "default"
  }
  data = {
    # Real contents may be referenced via other arguments
    ca_hash = module.ca.bundle_hash
  }
  lifecycle {
    replace_triggered_by = [
      module.ca.bundle_hash
    ]
  }
}

# Example 2: replace on a resource-attribute change
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = aws_acm_certificate.cert.arn

  default_action {
    type = "fixed-response"
    fixed_response {
      content_type = "text/plain"
      status_code  = 200
      message_body = "ok"
    }
  }

  lifecycle {
    replace_triggered_by = [
      aws_acm_certificate.cert.arn  # replace listener when the certificate is rotated
    ]
  }
}

Evaluation Order at Plan Time and What It Means on the Graph

After plan computes diffs, replace_triggered_by checks whether the value of each trigger reference has changed since the previous state, and flags the target resource for replacement if it has. So if refresh doesn't update the target, the change isn't seen.

Referencing a whole resource means "any planned change in that instance trips the trigger," while referencing an attribute restricts it to "a change in that specific attribute's value." Under count/for_each, evaluation happens per instance, so only the instances whose trigger changed are replaced.

  • Plan roughly proceeds in the order Refresh → Diff → Replace decision (aggregating replacement requirements).
  • If the referenced value is unchanged (for example, the hash is identical), no replacement occurs.
  • Make the reference as narrow as possible (at the attribute level) to avoid unnecessary replacements.
  • Augment with depends_on for ordering — replace_triggered_by alone does not guarantee creation order.

Minimizing evaluation when combined with for_each

# Each VM is replaced only when the base image digest changes
locals {
  vms = {
    web  = { size = "small" }
    api  = { size = "medium" }
  }
}

module "base_image" {
  source = "./modules/base_image"
  # output: digest
}

resource "azurerm_linux_virtual_machine" "vm" {
  for_each = local.vms

  name                = "${each.key}-vm"
  resource_group_name = azurerm_resource_group.rg.name
  size                = each.value.size
  # ... other required arguments

  lifecycle {
    replace_triggered_by = [
      module.base_image.digest
    ]
  }
}

Patterns That Pay Off in Practice: Certificates, Images, and Init-Time Dependencies

For certificates and images where the contents change but the reference name stays the same, you often want to err on the side of safety and force replacement, even if the provider would otherwise update in place. Components that pull external resources just once during initialization are especially good candidates for recreation.

Likewise, when you need to force a Deployment to roll on container image digest changes but the provider's diff detection occasionally misses it, replace_triggered_by gives you a design that swaps the resource reliably.

  • Certificate rotation: reference module.ca.bundle_hash to replace the affected components.
  • AMI / image refresh: reference module.image.digest to recreate nodes or instances.
  • Init-only dependencies: when a setting that's read only at creation changes, replace to guarantee it takes effect.

Example: replace the Deployment when the container image digest changes

module "artifact" {
  source = "./modules/artifact"
  # output: image_digest
}

resource "kubernetes_deployment" "app" {
  metadata { name = "app" }
  spec {
    replicas = 3
    selector { match_labels = { app = "app" } }
    template {
      metadata { labels = { app = "app" } }
      spec {
        container {
          name  = "app"
          image = "example.com/app@${module.artifact.image_digest}"
        }
      }
    }
  }
  lifecycle {
    replace_triggered_by = [
      module.artifact.image_digest
    ]
  }
}

Pitfalls and Anti-Patterns: Avoiding Unnecessary Replacement and Infinite Loops

Too coarse a target makes replace_triggered_by fire almost every run and balloons the plan. Too narrow a target won't replace when you expect. Tuning the reference granularity is everything. References to time-based or random values, which change "without you intending them to," cause unwanted replacement loops.

When you combine it with ignore_changes, replacement still runs whenever replace_triggered_by fires, even if ignore_changes is hiding the diff. The "intent to replace" takes precedence over diff suppression — keep that in mind.

  • Keep coarse references (whole-resource) to a minimum; prefer scoping to an attribute when possible.
  • Do not trigger off values that change on a schedule (like time_rotating).
  • When debugging, narrow plan to the specific instance (selective plan).
  • Design so that ignore_changes alone can't dodge a forced replacement.

Anti-pattern: referencing a value that changes on a schedule

# Bad: time_rotating changes its value on a fixed cadence, so replacement runs periodically
resource "time_rotating" "weekly" {
  rotation_days = 7
}

resource "some_resource" "target" {
  # ...
  lifecycle {
    replace_triggered_by = [
      time_rotating.weekly.id  # the value rotates and the resource is replaced every time
    ]
  }
}

Migrating from null_resource.triggers: Clearer Intent and Better Maintainability

Historically, recreations tied to external value changes were often emulated with null_resource's triggers, with other resources depending on it to force a reapply. That approach buries the intent indirectly and makes the dependency graph harder to read.

Migrating to replace_triggered_by expresses "which value change replaces which resource" directly in code, and plan output gets easier to read. Pair it with depends_on if you also need to control ordering.

  • Encoding the intent on the target resource itself is clearer than faking a trigger with triggers.
  • When migrating: 1) enumerate values that mirrored triggers, 2) move them onto the target resource as replace_triggered_by, 3) clean up ordering with depends_on.
  • Compare plans before and after to confirm no extra replacements creep in.

Migration example: from null_resource triggers to lifecycle.replace_triggered_by

# Old: using null_resource as a pseudo-trigger
resource "null_resource" "trigger" {
  triggers = {
    ca_hash = module.ca.bundle_hash
  }
}

# Old: the real resource depends on the trigger
resource "kubernetes_secret" "api" {
  # ...
  depends_on = [null_resource.trigger]
}

# New: consolidate the trigger onto the real resource
resource "kubernetes_secret" "api" {
  # ...
  lifecycle {
    replace_triggered_by = [module.ca.bundle_hash]
  }
  # If you also need ordering, add depends_on
  # depends_on = [module.ca]
}

Exam Checklist and Verification Workflow

The exam often probes the differences between replace_triggered_by and depends_on / ignore_changes, the granularity of resource vs. attribute references, and the per-instance evaluation under count/for_each. The comparison with null_resource.triggers also shows up frequently.

For real-world validation, build a small sample, exercise both "reference changed" and "reference unchanged" paths, and confirm that plan output shows "must be replaced." Checking behavior in refresh-only mode is also useful.

  • replace_triggered_by isn't an ordering control (it expresses the intent to replace).
  • Best practice: reference at the attribute level to suppress unnecessary replacement.
  • count/for_each is evaluated per instance.
  • Remember: ignore_changes cannot cancel a forced replacement.

Local verification commands

# Initial run
terraform init
terraform plan -out=tfplan
terraform apply tfplan

# Update the referenced value (e.g., a commit or version bump that changes a module output)

# Confirm the replacement is planned
terraform plan | grep -A2 "must be replaced" || true

# Confirm there are no remaining diffs
terraform plan -refresh-only

Check Your Understanding

Pro

問題 1

Module module.ca outputs a certificate bundle hash (bundle_hash), and kubernetes_secret.api must always be recreated when that value changes. Which Terraform configuration is correct?

  1. Set replace_triggered_by = [module.ca.bundle_hash] on the lifecycle block of kubernetes_secret.api
  2. Set depends_on = [module.ca.bundle_hash] on kubernetes_secret.api
  3. Create a null_resource whose triggers contains module.ca.bundle_hash and have kubernetes_secret.api depends_on it
  4. Set ignore_changes = [data] on the lifecycle block of kubernetes_secret.api

正解: A

lifecycle.replace_triggered_by is the right answer because it expresses the intent to replace directly. depends_on only controls ordering and does not force replacement. The null_resource + triggers combo is indirect and redundant. ignore_changes merely hides diffs and does not force replacement.

Frequently Asked Questions

What can be referenced in replace_triggered_by? Can variables or data sources be used?

Targets are references whose value change Terraform can detect at plan time: resource references, resource attribute references, and module outputs. A purely static value like a plain variable is hard to treat as an "external change" and also blurs the intent of the design. In practice, restrict yourself to resource, attribute, and module-output references.

When combined with ignore_changes, which one wins?

ignore_changes suppresses diffs on specific attributes, but if replace_triggered_by fires, the resource is replaced. Think of it as the "intent to replace" overriding the diff suppression.

Does a drift in the referenced target also trigger replacement?

If plan-time refresh updates the state with the target's current value and Terraform recognizes the value as changed, a replacement is planned. The converse is true: if plan cannot detect a change in the target, no replacement happens. Regular plan/refresh and monitoring are essential.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.