How to Fix Terraform 'Unsupported block type lifecycle' in a Module Call
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: A
lifecycleblock was placed directly inside amodule {}call. Terraform's HCL parser does not supportlifecycleas a valid block type at the module-call level — it is exclusively aresource-level meta-argument. - How to fix it: Remove the
lifecycleblock from themodule {}call entirely. If you needprevent_destroyorignore_changessemantics, implement them inside theresourceblocks within the module, or wrap the module's outputs in aresourcewith the correct lifecycle rules. - Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your broken
.tffile and get corrected HCL instantly without leaking your config to a third-party server.
The Incident (What Does the Error Mean?)
Here is the raw error Terraform emits during terraform plan or terraform apply:
Error: Unsupported block type
on main.tf line 12, in module "my_vpc":
12: lifecycle {
Blocks of type "lifecycle" are not expected here.
Immediate consequence: Terraform exits with a non-zero code before it evaluates any resource in your configuration. Your entire plan is dead. In a CI/CD pipeline this means the deployment job fails hard, blocking every downstream stage — staging promotion, integration tests, and production rollout all halt.
The Attack Vector / Blast Radius
This is a configuration correctness failure with a deceptively large blast radius:
- Pipeline lockout: Because Terraform validates HCL syntax before building the dependency graph, a single misplaced
lifecycleblock kills the entire root module run — not just the offending module. Every resource in that workspace is undeployable. - State drift risk: If an engineer works around this by manually applying partial configs or by commenting out the block without understanding why, the intended
prevent_destroyorignore_changesguard is silently dropped. A subsequentterraform applycan then destroy a production database or stateful workload that was supposed to be protected. - Misunderstood abstraction: Developers migrating from Pulumi or CDK often assume module-level lifecycle controls exist. They don't. The misconception leads to repeated broken commits and, worse, to teams believing
prevent_destroyis active when it is not — a false sense of safety around critical infrastructure. - Cascading in monorepos: In a Terragrunt or monorepo setup where dozens of modules share a root configuration, this single syntax error can block unrelated infrastructure stacks if they are wired into the same
terraform initworkspace.
How to Fix It (The Solution)
Basic Fix — Remove the Invalid Block
The immediate fix is surgical: delete the lifecycle block from the module call.
module "my_vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "production-vpc"
cidr = "10.0.0.0/16"
- lifecycle {
- prevent_destroy = true
- }
}
This unblocks terraform plan immediately. However, you have now lost whatever protection the lifecycle rule was intended to provide. Do not stop here.
Enterprise Best Practice — Enforce Lifecycle Inside the Resource
The lifecycle meta-argument must live inside the resource block that manages the actual infrastructure object. If you control the module source, add it there. If you are consuming a third-party module, use a moved block or a wrapper resource pattern.
Option A — Inside the module's internal resource (if you own the module source):
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr
+ lifecycle {
+ prevent_destroy = true
+ ignore_changes = [tags]
+ }
}
Option B — Using a null_resource / terraform_data guard at the root level (Terraform ≥ 1.4):
+# Sentinel guard — fails plan if someone tries to destroy the VPC module outputs
+resource "terraform_data" "vpc_destroy_guard" {
+ input = module.my_vpc.vpc_id
+
+ lifecycle {
+ prevent_destroy = true
+ }
+}
+
module "my_vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "production-vpc"
cidr = "10.0.0.0/16"
}
Option C — ignore_changes equivalent at the root using moved + lifecycle (Terraform ≥ 1.1):
For stateful resources where you want ignore_changes on a specific attribute managed by a module, override the resource directly after importing:
+resource "aws_db_instance" "primary" {
+ # Sourced via module, overridden here for lifecycle control
+ lifecycle {
+ ignore_changes = [engine_version, snapshot_identifier]
+ }
+}
Key rule to tattoo on your monitor:
lifecycleis valid inresource {}anddata {}blocks. It is never valid inmodule {},variable {},output {},locals {}, orprovider {}blocks.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
Do not rely on terraform plan in your CD pipeline to catch this. Catch it at the PR stage.
1. terraform validate as a Pre-Commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.5
hooks:
- id: terraform_validate
- id: terraform_tflint
terraform validate will catch Unsupported block type errors before any code is pushed.
2. TFLint with the AWS Ruleset
# .tflint.hcl
plugin "terraform" {
enabled = true
preset = "recommended"
}
Run in CI:
tflint --recursive --format=compact
TFLint's terraform_required_version and block-type rules flag misplaced meta-arguments before plan is ever invoked.
3. Checkov Policy (OPA-compatible)
Checkov check CKV_TF_1 and custom OPA policies can enforce that lifecycle blocks only appear inside resource definitions:
checkov -d . --framework terraform --check CKV_TF_1
For OPA/Conftest, add a custom policy:
# policy/lifecycle_placement.rego
package terraform
deny[msg] {
block := input.configuration.root_module.module_calls[_]
block.lifecycle
msg := sprintf("Module call '%v' contains an illegal lifecycle block. Move it inside the target resource.", [block])
}
4. Sentinel Policy (Terraform Enterprise / HCP Terraform)
# sentinel/lifecycle-in-module-call.sentinel
import "tfplan/v2" as tfplan
main = rule {
all tfplan.module_calls as _, call {
not (call contains "lifecycle")
}
}
Enforce this policy as a hard-mandatory check in your HCP Terraform workspace to block any run where a module call contains a lifecycle block.