Initializing Enclave...

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 lifecycle block was placed directly inside a module {} call. Terraform's HCL parser does not support lifecycle as a valid block type at the module-call level — it is exclusively a resource-level meta-argument.
  • How to fix it: Remove the lifecycle block from the module {} call entirely. If you need prevent_destroy or ignore_changes semantics, implement them inside the resource blocks within the module, or wrap the module's outputs in a resource with the correct lifecycle rules.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your broken .tf file 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 lifecycle block 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_destroy or ignore_changes guard is silently dropped. A subsequent terraform apply can 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_destroy is 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 init workspace.

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: lifecycle is valid in resource {} and data {} blocks. It is never valid in module {}, variable {}, output {}, locals {}, or provider {} 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.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →