Terraform 1.16 beta: what destroy = false, action on_failure and module import blocks actually change

Terraform 1.16.0-beta1 shipped 23 July 2026. Here is what each new lifecycle rule actually does.

Read time
18 min
Word count
2.9K
Sections
13
FAQs
8
Share
Terraform 1.16 lifecycle changes: destroy = false, action on_failure and module import blocks
Terraform 1.16.0-beta1 changed what a lifecycle block can do.
On this page · 13 sections
  1. The version confusion, settled first
  2. Action triggers grow up: halt, taint, continue
  3. Import blocks inside modules
  4. Everything else in the beta, ranked by whether you will notice
  5. What 1.15 already gave you, and what it did not
  6. The cost side nobody puts in the release notes
  7. Where this sits next to OpenTofu
  8. The agent angle, and why lifecycle rules suddenly matter more
  9. India-specific considerations
  10. An upgrade plan you can actually run
  11. FAQ
  12. How eCorpIT can help
  13. References

Summary. Terraform 1.16.0-beta1 was published on 23 July 2026, roughly three months after Terraform 1.15.0 reached general availability on 29 April 2026 and 1.15.1 followed on 1 May 2026. The beta adds 6 new features and 12 enhancements to the HashiCorp changelog, and the one drawing the most attention is a single line: resource lifecycle blocks now support destroy = false. That rule is widely and wrongly attributed to Terraform 1.14. In Terraform 1.15.x, destroy is documented only inside a removed block, so a team that pins ~> 1.15 and copies a destroy = false snippet into a resource block gets an error, not protection. The other changes that matter to production teams are on_failure modes of halt, taint or continue for action triggers, import blocks inside modules, before_destroy and after_destroy action events, terraform graph -format=mermaid, and -json output for state show and workspace list. None of this is free to adopt: HCP Terraform bills on managed resources, and Spacelift's pricing analysis, updated 6 August 2026, puts the Essentials rate at $0.0001359 per managed resource per hour, or about $978.48 a month for 10,000 resources running 24x7.

That last number is why this release deserves a careful read rather than a skim. Every new lifecycle rule changes what sits in state, and what sits in state is what you pay for.

The version confusion, settled first

Three separate claims are circulating about destroy = false, and only one of them is correct.

The correct one comes from the Terraform v1.16.0-beta1 release notes, dated 23 July 2026, which list under ENHANCEMENTS: "Resource lifecycle blocks now support destroy = false to prevent a resource from being destroyed." The change is tracked as issue 38784.

The incorrect ones say the rule arrived in 1.14 or 1.15. Check the shipped documentation and the claim collapses. The lifecycle meta-argument reference for v1.15.x, the version marked "latest" on HashiCorp Developer as of 9 August 2026, describes destroy in one sentence: "Set to false to remove a resource from state without destroying the actual infrastructure resource. You can only use this rule in removed block." Same keyword, different block, opposite intent. In a removed block it means "forget this resource without deleting it". In a resource block, from 1.16, it means "refuse to delete this resource".

If you are running 1.15.x today, the protection you have is prevent_destroy, and it has a documented hole. HashiCorp's own reference states that Terraform "does not explicitly record a resource's lifecycle rule to state" except for create_before_destroy, so "Terraform destroys the actual infrastructure during an apply operation if you remove the resource's configuration, even if prevent_destroy is enabled." Delete the block, lose the guard. That is the failure mode most teams have actually hit.

Rule Introduced Valid in What it does Survives config deletion
prevent_destroy Pre-1.0 resource Rejects plans that would destroy the object No — removing the block removes the guard
destroy = false 1.16.0-beta1 (23 Jul 2026) resource Prevents the resource from being destroyed Not yet documented for GA
destroy = false Earlier removed Drops the resource from state, leaves infrastructure alive Not applicable
create_before_destroy Pre-1.0 resource Creates the replacement before destroying the original Yes — recorded in state
ignore_changes Pre-1.0 resource Skips update planning for named attributes No

The practical read: treat destroy = false as a 1.16 feature, keep prevent_destroy on your stateful resources until 1.16 is GA in your pipeline, and pair either one with a removed block workflow when you genuinely need to hand a resource to another owner.

Action triggers grow up: halt, taint, continue

Terraform actions let a provider expose an imperative operation that runs outside normal create, read, update and delete. You bind them with an action_trigger block inside lifecycle. Per the v1.15.x reference, action_trigger takes three arguments: events (required list), condition (optional expression) and actions (required ordered list). In 1.15.x the documented events are before_create, after_create, before_update and after_update.

The 1.16 beta extends this in two ways that change how you design a pipeline. First, actions can now use before_destroy and after_destroy events, tracked as issue 38668. Second, and more consequential, "Resource action triggers can now use on_failure modes of halt, taint, or continue" (issue 38722).

Before 1.16, a failed action left you with one behaviour and no dial. Now the failure semantics are yours to choose:


            resource "aws_ecs_service" "api" {
  name            = "api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.api.arn

  lifecycle {
    action_trigger {
      events     = [after_update]
      on_failure = halt
      actions    = [action.aws_lambda_invoke.smoke_test]
    }

    action_trigger {
      events     = [after_create]
      on_failure = continue
      actions    = [action.aws_lambda_invoke.warm_cache]
    }
  }
}
          

The judgement call is which mode belongs where. A smoke test that fails should stop the run, so halt. A cache warm that fails is noise, so continue. taint sits between them: the action failed, the resource is suspect, mark it for replacement on the next apply rather than pretending the deploy was clean. Getting this wrong in either direction is expensive — continue on a verification step ships broken infrastructure quietly, and halt on a best-effort side task blocks releases for no reason.

Two more action changes in the beta are worth knowing. Action configurations in Stacks now have access to a caller symbol containing the object value of the calling resource (issue 38668), and -invoke can be combined with -target to specify the calling resource instance when multiple resources trigger the same action (issue 38845). The beta also fixes a real bug here: "Actions are now invoked with respect to all resource dependencies." If you built anything on actions in 1.15, re-read your dependency assumptions.

There is also a crash fix carried in 1.15.1, released 1 May 2026: "Fixed crash when configuration has an invalid action_trigger nested block in data or ephemeral lifecycle blocks" (issue 38402). Teams on 1.15.0 who are experimenting with actions should take the patch.

Import blocks inside modules

import blocks inside modules are now supported (issue 38352). This is the change with the widest blast radius for teams that inherited infrastructure created by hand, by another tool, or by an earlier IaC generation.

Until now, an import block had to live in the root module. If your platform team distributes a module for, say, a standard VPC layout, and a business unit already has that VPC clicked into existence, the import had to be written at the root, referencing addresses inside the module. That works, but the knowledge of what to import lives in the wrong place — with the consumer, not the module author.


            # modules/network/import.tf
variable "existing_vpc_id" {
  type    = string
  default = null
}

import {
  for_each = var.existing_vpc_id == null ? [] : [var.existing_vpc_id]
  to       = aws_vpc.main
  id       = each.value
}
          

The beta pairs this with two related fixes: "import blocks now correctly respect provider local names" (issue 38338) and "Terraform now returns the correct error when an import target exists in state but has no corresponding configuration" (issue 38782). The second one matters if you have ever chased a confusing message during a brownfield migration.

One planning note for cost. Import brings resources under management, and under HCP Terraform's Resources Under Management model, that is a billing event. Spacelift's analysis notes that a resource is counted "if it's in an HCP-Terraform-managed state file and in the state it has mode = "managed"", starting from the first terraform plan or terraform apply, and that null_resource and terraform_data are excluded. Security group rules and S3 lifecycle rules each count individually. A brownfield import of a mature AWS account can add thousands of billable resources in one afternoon.

Everything else in the beta, ranked by whether you will notice

Change Type Who it affects Effort to adopt
destroy = false in resource lifecycle Enhancement Anyone protecting stateful resources Low — one line, but wait for GA
on_failure modes for action triggers Feature Teams already using actions Medium — requires a policy per trigger
import blocks inside modules Feature Module authors, brownfield migrations Medium — module redesign
terraform graph -format=mermaid Enhancement Anyone documenting dependencies Low
-json for state show and workspace list Enhancement CI/CD and tooling authors Low
terraform console -scope=<module> Enhancement Anyone debugging module internals Low
terraform_data store block Feature Teams handling ephemeral secrets Medium
Planned private data for providers Feature Provider developers None for consumers
Linux s390x (zLinux) binary Feature Mainframe-adjacent shops Low
contains() accepts null Enhancement Anyone writing defensive HCL Low
bastion_host_key now applied by provisioners Upgrade note Anyone using SSH provisioners Check before upgrading

Three of these deserve a sentence each.

terraform graph -format=mermaid (issue 38719) is the smallest change with the best return. Terraform's DOT output has always needed Graphviz to be useful. Mermaid renders natively in GitHub, GitLab and most documentation platforms, so a dependency graph can now live in a pull request description instead of a stale wiki page.

terraform console -scope=<module address> (issue 31861) closes a request that has been open for years. Debugging an expression inside a module previously meant reproducing the module's variable context by hand in the root console. Now you point the console at the module instance.

The store block in terraform_data (issue 38298) can hold ephemeral and sensitive values across plan and apply. Combined with the provider private-data change (issue 37986), this is the beta's quiet theme: state is becoming more able to carry things it previously could not, without those things landing in the state file as plaintext.

And read the upgrade note before you touch a bastion: "bastion_host_key is now correctly applied by provisioners. Review your provisioner configurations to verify the configured key is correct before upgrading" (issue 38318). A key that was silently ignored is now enforced. Configurations that "worked" because the value was wrong will start failing.

What 1.15 already gave you, and what it did not

A fair amount of the confusion about 1.16 comes from people conflating it with 1.15, which shipped on 29 April 2026 and was covered in a HashiCorp post on 13 May 2026 by Apoorva Murthy and Jacob Plicque.

Terraform 1.15's headline items were dynamic module sources, variable and output deprecation, and inline type conversion. On dynamic sources, the HashiCorp post states: "With the Terraform 1.15 release, practitioners can now use variables within Terraform dependencies, encompassing aspects such as module sources and versions." The mechanism is a new const attribute on variables that signals whether the variable can be used during terraform init. It cannot be combined with sensitive or ephemeral.


            variable "folder" {
  type  = string
  const = true
}

module "zoo" {
  source = "./${var.folder}"
}
          

The deprecation feature is the one platform teams under-use. A deprecated attribute on a variable or output emits a warning diagnostic during validation, which gives module authors a way to retire an interface without breaking every consumer on the same day. Root-module outputs are stricter: using a deprecated attribute there produces an error, not a warning.

1.15 also brought native Windows ARM64 binaries, S3 backend authentication via aws login (AWS CLI v2.32.0 and later), explicit type constraints on output blocks, functions inside mock blocks in the test framework, and validation blocks for Stacks variables.

What 1.15 did not bring is destroy = false on resources, on_failure, module-level imports or Mermaid graphs. If a blog post tells you otherwise, check the changelog for the tag you are pinning.

The cost side nobody puts in the release notes

Every feature above changes what Terraform manages, and HCP Terraform charges per managed resource. Independent pricing analysis published by Spacelift and updated on 6 August 2026 reports the current shape: a Free tier capped at 500 managed resources per organisation with 1 concurrent run, then Essentials at about $0.10 per managed resource per month, Standard at about $0.47 and Premium at about $0.99. Billing runs on hourly peak managed resources, with partial hours billed as full hours.

Scale Plan Reported rate Monthly cost
Under 500 resources Free $0 $0
1,000 resources Standard $0.47 per resource-month About $470
1,000 resources Premium $0.99 per resource-month About $990
10,000 resources Essentials $0.0001359 per resource-hour $978.48 (30-day month)
10,000 resources Standard $0.47 per resource-month About $4,700
Self-managed Terraform Enterprise From $15,000/year Five workspaces included

Flavius Dinu, Developer Advocate at Mirantis and a Docker Captain, writing in Spacelift's 2026 pricing breakdown, sums up the complaint bluntly: "The key fear for customers is the inability to predict their Terraform Cloud bills". He is writing for a competitor, so weigh it accordingly, but the mechanism he describes is documented behaviour, not opinion — a per-resource meter that counts each security group rule separately is hard to forecast when a module refactor can double the resource count without changing a single piece of infrastructure.

HashiCorp's own pricing page, checked on 9 August 2026, advertises a $500 starting credit across the IBM HashiCorp Cloud Platform and routes detailed rates to an IBM pay-as-you-go pricing table. The legacy Free plan reached end of life on 31 March 2026, with remaining organisations moved to the enhanced Free tier of 500 managed resources and unlimited users.

The engineering conclusion: before you import a brownfield estate under a module-level import block, count what you are about to put in state. The real cost is usually the migration, not the code.

Where this sits next to OpenTofu

Teams that forked to OpenTofu after the licence change have their own feature ladder. OpenTofu 1.11.0 introduced ephemeral values, ephemeral resources and write-only attributes, plus an enabled meta-argument for resources and modules alongside count and for_each. It added S3 state storage in the eusc-de-east-1 region of the AWS European Sovereign Cloud, an azure_vault key provider for state and plan encryption keys, and dropped SHA-1 signatures in TLS handshakes per RFC 9155. The 1.11.x series is supported until 1 August 2026, with 1.12 now current.

The two projects are diverging on lifecycle design rather than converging. OpenTofu's enabled meta-argument and Terraform's destroy = false solve adjacent problems in different places in the language. If you are still deciding, our comparison of Terraform vs OpenTofu for 2026 standardisation walks the migration path in detail.

The agent angle, and why lifecycle rules suddenly matter more

On 5 August 2026, HashiCorp published a post arguing that HCP Terraform is the control plane for AI-driven infrastructure. Its authors, Simon Lynch and Aaron Evans of HashiCorp, describe the shift precisely: "Code that took days now takes seconds, so the bottleneck has moved from authoring to verification and approval."

That reframes every rule in this article. A destroy = false on a production database is a guardrail against a human typo today. Against an agent running a plan-execute-observe loop at machine speed, it is one of the few things standing between a hallucinated refactor and a deleted RDS instance. The HashiCorp post is blunt about the risk: "Without guardrails, an AI agent with write access to your infrastructure will amplify every gap in your IaC maturity through hallucinated output, ungated changes, over-broad access, and unbounded blast radius."

The post lays out five control layers — provenance, policy, identity, isolation and audit — and notes that mandatory policies cannot be bypassed by an agent lacking policy-admin permissions, because the gate lives in the platform layer rather than in the agent. Its operating rule is worth copying into your own runbook: "The agent can propose. It should not decide."

For teams building agent-assisted delivery pipelines, this connects directly to how you govern AI coding agent rollouts and how you structure release engineering and CI/CD platforms.

India-specific considerations

Indian platform teams face two pressures at once here. The first is cost. A rupee-denominated budget against a dollar-denominated per-resource meter means the effective HCP Terraform bill moves with the exchange rate on top of resource growth. At roughly ₹88 to the dollar in August 2026, a 10,000-resource Essentials bill of $978.48 a month lands near ₹86,000 a month before tax, for tooling that manages infrastructure you are already paying a cloud provider for. Many Indian teams run a self-hosted CI runner plus S3 or Azure Blob state instead, and spend the difference on engineers.

The second is data residency. Under the Digital Personal Data Protection Act 2023, where your Terraform state lives is a real question, because state files routinely contain identifiers, connection strings and occasionally worse. The 1.16 store block in terraform_data and the provider private-data change help keep sensitive values out of plaintext state, but they do not change where the state file physically sits. If you are designing for residency, decide the backend region before you decide the feature set. Our guide to cutting cloud spend for Indian teams covers the cost half of that trade-off, and Azure landing zone and Terraform IaC modernisation covers the architecture half.

An upgrade plan you can actually run

The beta is a beta. Do not put 1.16.0-beta1 in a production pipeline. Do use it now to find out what will break in three months.

  1. Pin your current version explicitly in required_version. A range like >= 1.15 will pull the 1.16 GA the day it lands.
  1. Run terraform plan against a copy of production state with the 1.16 beta binary in a scratch workspace. Compare plan output for drift.
  1. Audit every provisioner block for bastion_host_key. The upgrade note is explicit that behaviour changed.
  1. Inventory your action_trigger blocks and assign each an on_failure mode deliberately. The default is not a decision.
  1. If you plan to use module-level import, count the resources first and price them against your HCP Terraform tier.
  1. Replace Graphviz steps in documentation pipelines with terraform graph -format=mermaid once you are on 1.16.
  1. Keep prevent_destroy in place until destroy = false ships in a GA release and you have tested what happens when the block is removed.

If you are also planning a provider upgrade in the same window, sequence it separately. Our write-up on the AzureRM 5.0 breaking changes and migration explains why stacking a core upgrade and a provider major on the same change window turns two tractable problems into one intractable one. The same logic applies to cluster work such as a Kubernetes 1.35 and containerd 2.0 migration.

FAQ

How eCorpIT can help

eCorpIT is a CMMI Level 5 and ISO 27001:2022 certified engineering organisation in Gurugram, and our platform teams run Terraform upgrades, provider major migrations and brownfield state imports as scoped engagements rather than as background work squeezed between sprints. We size the managed-resource count before an import so the tooling bill does not arrive as a surprise, and we sequence core, provider and cluster upgrades so a single change window fails in only one way. If you are planning a 1.16 adoption or an IaC consolidation, talk to our engineering team.

References

  1. Release v1.16.0-beta1 - hashicorp/terraform - GitHub, 23 July 2026.
  1. Release v1.15.1 - hashicorp/terraform - GitHub, 1 May 2026.
  1. lifecycle meta-argument reference - HashiCorp Developer, v1.15.x documentation.
  1. New in Terraform 1.15: Dynamic sources, variable deprecation, and more - Apoorva Murthy and Jacob Plicque, HashiCorp, 13 May 2026.
  1. HCP Terraform is the control plane for AI-driven infrastructure - Simon Lynch and Aaron Evans, HashiCorp, 5 August 2026.
  1. Terraform Cloud/Enterprise Pricing - Tiers Overview 2026 - Flavius Dinu, Spacelift, updated 6 August 2026.
  1. HashiCorp Product Pricing - HashiCorp, accessed 9 August 2026.
  1. Releases - hashicorp/terraform - GitHub release index.
  1. What's new in OpenTofu - OpenTofu documentation.
  1. Releases - opentofu/opentofu - GitHub.
  1. Invoke actions with Terraform - HashiCorp Developer tutorial.
  1. resource block reference - HashiCorp Developer.

Last updated: 9 August 2026.

Frequently asked

Quick answers.

01 Does destroy = false work in Terraform 1.15?
No. The v1.15.x lifecycle reference documents destroy only inside a removed block, where setting it to false drops a resource from state without deleting the infrastructure. Support for destroy = false inside a resource lifecycle block appears in the Terraform 1.16.0-beta1 changelog published on 23 July 2026.
02 What is the difference between destroy = false and prevent_destroy?
Both aim to stop deletion, but prevent_destroy has a documented gap: Terraform does not record lifecycle rules to state except create_before_destroy, so removing the resource block removes the guard and the apply proceeds. The 1.16 beta introduces destroy = false as a separate resource-level rule with its own tracking issue, 38784.
03 What do the action trigger on_failure modes do?
Terraform 1.16.0-beta1 adds halt, taint and continue. Halt stops the run when the action fails. Continue lets the run proceed, which suits best-effort side tasks. Taint marks the resource for replacement on the next apply, which suits cases where a failed action leaves the resource in a questionable state.
04 Can I now put import blocks inside a Terraform module?
Yes, from Terraform 1.16.0-beta1, tracked as issue 38352. Module authors can ship the import logic alongside the resources it targets rather than pushing it to the root module. The same beta fixes provider local-name handling in import blocks and improves the error when an import target exists in state without configuration.
05 How much does HCP Terraform cost per resource in 2026?
Spacelift's pricing analysis, updated 6 August 2026, reports a free tier of 500 managed resources, Essentials from about $0.10 per managed resource per month, Standard about $0.47 and Premium about $0.99, billed on hourly peak resources. At 10,000 resources on Essentials that works out to $978.48 a month over a 30-day month.
06 When did Terraform 1.15 come out and what did it add?
Terraform 1.15.0 was released on 29 April 2026, with 1.15.1 following on 1 May 2026. It added dynamic module sources using a new const variable attribute, deprecation warnings for variables and outputs, an inline convert function, native Windows ARM64 binaries and S3 backend support for AWS aws login credentials.
07 What is terraform graph -format=mermaid useful for?
It outputs the dependency graph in Mermaid syntax instead of only DOT, tracked as issue 38719 in the 1.16 beta. Mermaid renders natively in GitHub, GitLab and most documentation tools, so a dependency diagram can be generated in CI and posted into a pull request rather than needing a local Graphviz install.
08 Should I upgrade to Terraform 1.16 beta now?
Not in production. Run the beta binary against a copy of production state in a scratch workspace to find drift and breakage early, pin required_version so the GA release does not arrive unannounced, and audit bastion_host_key in provisioner blocks, which the beta's upgrade notes flag as a behaviour change.

About the author

Manu Shukla

Founder & Director

Founder of eCorpIT. Hands-on engineer leading senior-only delivery for AI apps, custom software, and cloud systems for global clients.

Subscribe

One engineering note a week. No fluff, no spam.

Senior-architect playbooks on AI agents, mobile apps, cloud, security, data, and marketing — delivered every Wednesday.

Past the reading

Read enough. Let's build something.

A senior architect responds in 24 working hours with scope, indicative cost, and a timeline. NDA before any technical conversation.