How to Fix Terraform 'Error: Unsupported attribute' for Module Output References
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: A resource or root module is referencing
module.<name>.<attribute>but that attribute is not declared in the called module'soutputs.tf, causingterraform planto hard-fail. - How to fix it: Declare the missing output in the child module's
outputs.tf, or correct the attribute name in the reference to match an existing output. - Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your module source and the referencing resource, and get the corrected output block generated instantly.
The Incident (What Does the Error Mean?)
Raw error from terraform plan:
Error: Unsupported attribute
on main.tf line 22, in resource "aws_security_group_rule" "allow":
22: source_security_group_id = module.vpc.security_group_id
This object does not have an attribute named "security_group_id".
Terraform's module system is strict at plan time. When you reference module.vpc.security_group_id, Terraform looks up the outputs.tf of the ./modules/vpc directory for an output "security_group_id" block. If it doesn't exist — whether because it was never written, was renamed, or the wrong module source is pinned — the entire plan aborts. No partial plan is generated. All downstream resources dependent on this value are also blocked.
The Attack Vector / Blast Radius
This is not just a syntax annoyance. The blast radius is significant in team environments:
- CI/CD pipelines fail hard. A
terraform planexit code 1 blocks merges, deployments, and automated apply workflows in Atlantis, Terraform Cloud, or GitHub Actions. - Module versioning drift. The most dangerous variant: your root module pins
source = "git::...?ref=v2.1.0"but the team upgraded the child module to v3.0.0 where an output was renamed or removed. The reference silently breaks only when someone runs plan against a new workspace or after aterraform init -upgrade. - Cascading dependency failure. If
module.vpc.security_group_idfeeds into 6 downstream resources, all 6 are unplannable. Terraform does not gracefully skip them — the entire graph halts. - Refactoring without output hygiene. Engineers frequently refactor internal module resources (e.g., splitting one SG into multiple) and forget that external callers depend on specific output names. There is no compile-time contract enforcement by default.
How to Fix It (The Solution)
Basic Fix: Declare the Missing Output in the Child Module
If the attribute genuinely exists as a resource attribute inside the module but was never exported:
# modules/vpc/outputs.tf
+ output "security_group_id" {
+ description = "The ID of the primary VPC security group."
+ value = aws_security_group.main.id
+ }
If the output was renamed in the module (e.g., from security_group_id to primary_sg_id), fix the caller:
# root/main.tf
resource "aws_security_group_rule" "allow" {
- source_security_group_id = module.vpc.security_group_id
+ source_security_group_id = module.vpc.primary_sg_id
}
Enterprise Best Practice: Output Contract Validation with terraform-docs and Variable Validation
The root problem is no enforced contract between module producer and consumer. Fix this structurally:
Step 1 — Use explicit output descriptions and enforce them in code review via terraform-docs:
# modules/vpc/outputs.tf
- output "sg_id" {
- value = aws_security_group.main.id
- }
+ output "security_group_id" {
+ description = "Primary security group ID. Stable interface — do not rename without major version bump."
+ value = aws_security_group.main.id
+ precondition {
+ condition = aws_security_group.main.id != ""
+ error_message = "Security group was not created successfully."
+ }
+ }
Step 2 — Pin module versions explicitly and document breaking output changes in CHANGELOG:
# root/main.tf
module "vpc" {
- source = "git::https://github.com/org/terraform-aws-vpc.git"
+ source = "git::https://github.com/org/terraform-aws-vpc.git?ref=v2.1.0"
+ # OUTPUT CONTRACT: expects security_group_id (added v1.4.0)
}
Step 3 — Add a terraform validate gate in CI before plan:
# .github/workflows/terraform.yml
steps:
+ - name: Terraform Validate
+ run: terraform validate
- name: Terraform Plan
run: terraform plan
terraform validate catches unsupported attribute references without needing cloud credentials, making it a fast, cheap pre-flight check.
💡 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
Don't rely on engineers remembering to check output names. Enforce it in the pipeline:
1. terraform validate as a required pre-plan step
Runs the full type-checker and reference resolver without hitting the API. Zero-cost, catches this exact error class in under 2 seconds.
2. Checkov — scan for undeclared outputs in module calls
checkov -d . --check CKV_TF_1
Checkov's CKV_TF_1 enforces module source pinning, reducing version-drift-induced output breakage.
3. tflint with the terraform ruleset
tflint --enable-rule=terraform_module_pinned_source
Install the tflint-ruleset-terraform plugin. The terraform_deprecated_interpolation and module reference rules will flag mismatched output references statically.
4. OPA/Conftest policy for output naming conventions
# policy/module_outputs.rego
package terraform
deny[msg] {
output := input.configuration.root_module.module_calls[_]
# Enforce that any module producing a security group exports a canonical name
not output.outputs["security_group_id"]
msg := "Module must export 'security_group_id' as a stable output interface."
}
Run with conftest test --policy policy/ plan.json against the JSON plan output.
5. terraform-docs in pre-commit hooks
Auto-generates README tables of all inputs/outputs. Forces engineers to see the full output surface area before committing module changes, making accidental renames visible in PR diffs.
# .pre-commit-config.yaml
- repo: https://github.com/terraform-docs/terraform-docs
hooks:
- id: terraform-docs-go
args: ["markdown", "--output-file", "README.md", "."]