Initializing Enclave...

How to Fix Terraform 'Unsupported block type dynamic' Error in Resource Blocks

Threat/Impact Level: MEDIUM | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins


TL;DR

  • What broke: Terraform encountered a dynamic block inside a resource attribute or nested block position that the provider schema does not expose as a repeatable block type — or the dynamic keyword was used at the top-level resource argument level where it is syntactically illegal.
  • How to fix it: Move the dynamic block so it wraps only a legitimate repeatable nested block (e.g., ingress, egress, tag, setting) declared in the provider schema. Replace top-level argument iteration with for expressions or for_each on the resource itself.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your failing .tf block and get corrected HCL without sending your config to a third-party server.

The Incident (What Does the Error Mean?)

Raw error output:

Error: Unsupported block type

  on main.tf line 14, in resource "aws_security_group" "this":
  14:   dynamic "description" {

Blocks of type "description" are not expected here.

Terraform's type-checker validates every block label against the provider schema at terraform plan time — before any API call is made. When it sees dynamic "description", it looks up whether description is a repeatable nested block in the aws_security_group schema. It isn't — description is a scalar string argument, not a block. The plan exits non-zero immediately. No infrastructure change occurs, but your entire pipeline is blocked.

The same error fires in three other common scenarios:

  1. dynamic wrapping a top-level resource argument (e.g., dynamic "bucket" {} inside aws_s3_bucket).
  2. dynamic used inside a locals or variable block — neither supports it.
  3. Provider version mismatch: a block that was repeatable in provider v3.x was flattened to a single block or argument in v4.x, breaking existing dynamic usage.

The Attack Vector / Blast Radius

This is a pipeline-blocking syntax error, not a runtime security flaw — but the blast radius in a CI/CD context is significant:

  • Deployment freeze: Every terraform plan and terraform apply in the affected workspace fails. If this lands in a shared root module used by 15 teams via Terragrunt, all 15 stacks are frozen.
  • Drift accumulation: While the pipeline is blocked, engineers resort to manual console changes to meet deadlines. Those changes are now outside state. When the fix lands, terraform apply may destroy manually-created resources.
  • Masked cascading errors: A syntax error at line 14 prevents Terraform from evaluating lines 15–400. Other misconfigurations (open security groups, unencrypted buckets) are invisible until this is resolved — giving a false sense of a "one-bug" problem.
  • Module consumers broken silently: If the broken dynamic block lives inside a published Terraform module and a consumer pins to source = "git::...?ref=main" without a version tag, the breakage propagates to all consumers on next terraform init -upgrade.

How to Fix It (The Solution)

Identify the Offending Block

Run this to get the full schema for the resource and confirm which nested blocks are actually repeatable:

terraform providers schema -json | jq '.provider_schemas[].resource_schemas["aws_security_group"].block.block_types'

Only keys returned under block_types (not attributes) are valid dynamic block labels.


Basic Fix — Wrong Target Attribute

The most common mistake: wrapping a scalar attribute with dynamic instead of a nested block_type.

 resource "aws_security_group" "this" {
   name   = var.name
-  dynamic "description" {
-    for_each = var.descriptions
-    content {
-      description = description.value
-    }
-  }
+  description = var.descriptions[0]  # scalar — pick one value or join them
 }

If you genuinely need to iterate over ingress rules (a real block_type):

 resource "aws_security_group" "this" {
   name        = var.name
   description = var.description
+  dynamic "ingress" {
+    for_each = var.ingress_rules
+    content {
+      from_port   = ingress.value.from_port
+      to_port     = ingress.value.to_port
+      protocol    = ingress.value.protocol
+      cidr_blocks = ingress.value.cidr_blocks
+    }
+  }
 }

Enterprise Best Practice — Provider Version Pin + Schema Validation Gate

Provider schema changes between major versions silently invalidate dynamic blocks. Lock the provider and add a schema assertion:

 terraform {
   required_providers {
     aws = {
       source  = "hashicorp/aws"-
-      version = ">= 3.0"
+      version = "~> 5.0"  # pin to known-good major; test upgrades in isolation
     }
   }
 }

For modules consumed by multiple teams, add an explicit lifecycle precondition to fail fast with a human-readable message instead of a cryptic schema error:

 resource "aws_security_group" "this" {
   name        = var.name
   description = var.description
+
+  lifecycle {
+    precondition {
+      condition     = length(var.ingress_rules) > 0
+      error_message = "ingress_rules must contain at least one rule object. Received empty list."
+    }
+  }
+
   dynamic "ingress" {
     for_each = var.ingress_rules
     content {
       from_port   = ingress.value.from_port
       to_port     = ingress.value.to_port
       protocol    = ingress.value.protocol
       cidr_blocks = ingress.value.cidr_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

Gate 1 — terraform validate as the first pipeline step (non-negotiable):

# .github/workflows/tf.yml
- name: Validate
  run: terraform validate
  working-directory: ./infra

This catches the Unsupported block type error in under 2 seconds, before plan wastes API quota.

Gate 2 — tflint with provider plugin for schema-aware linting:

tflint --init
tflint --plugin-dir ~/.tflint.d/plugins

tflint's AWS ruleset (terraform_aws_provider_*) flags dynamic blocks targeting scalar attributes before validate even runs.

Gate 3 — Checkov policy for module hygiene:

checkov -d . --check CKV_TF_1  # enforces module version pinning, preventing schema drift

Gate 4 — OPA/Conftest policy to block unpinned provider versions (the root cause of schema drift):

# policy/tf_provider_pin.rego
package terraform.providers

deny[msg] {
  provider := input.configuration.provider_config[_]
  not regex.match(`^~> \d+\.\d+$`, provider.version_constraint)
  msg := sprintf("Provider '%v' must use pessimistic constraint (~> X.Y). Got: %v", [provider.name, provider.version_constraint])
}
conftest test plan.json --policy policy/

Recommended pipeline order:

tflint → terraform validate → conftest → terraform plan → checkov (on plan JSON) → terraform apply

Each gate is cheaper than the one after it. Kill bad configs at tflint, not at apply.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →