How to Fix 'Unsupported Attribute id' on aws_instance After Terraform 0.15 Upgrade
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: Terraform 0.15 tightened attribute validation. References to
aws_instance.<name>.idinsidemoduleoutputs,count-indexed resources, or cross-module wiring fail withUnsupported attribute 'id'when the reference path is structurally incorrect or the resource block is wrapped infor_each/countwithout proper index syntax. - How to fix it: Audit every
aws_instancereference. If usingcount, useaws_instance.<name>[count.index].id. If usingfor_each, useaws_instance.<name>[each.key].id. If inside a module output, ensure the output block explicitly exportsvalue = aws_instance.<name>.id. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your config and get corrected HCL without sending your AMI IDs or subnet data anywhere.
The Incident (What Does the Error Mean?)
Raw error output from terraform plan or terraform apply:
Error: Unsupported attribute
on main.tf line 42, in resource "aws_security_group" "web":
42: description = aws_instance.web.id
This object does not have an attribute named "id".
Or the variant that appears in module wiring:
Error: Unsupported attribute
on modules/compute/outputs.tf line 3, in output "instance_id":
3: value = aws_instance.app.id
This object does not have an attribute named "id".
Immediate consequence: terraform plan exits non-zero. Your entire pipeline is blocked. No infrastructure changes can be applied until this is resolved. In a GitOps workflow, every PR targeting this module is now failing.
The root cause is not that aws_instance lost its id attribute — it hasn't. The error surfaces because Terraform 0.15 introduced a stricter graph evaluation pass. When a resource uses count or for_each, the resource reference itself becomes a list or map, not a single object. Accessing .id directly on the collection — rather than on an indexed element — is what triggers this error.
The Attack Vector / Blast Radius
This is a pipeline-killing syntax regression, not a runtime security issue, but the blast radius is significant:
- Every downstream module that consumes the broken output is now blocked. A single malformed
outputs.tfin a sharedcomputemodule can take down 10+ environment pipelines simultaneously. - State drift risk: Engineers under pressure will attempt
terraform apply -targetworkarounds. This creates partial state, where some resources are provisioned without their dependent configurations, leading to security groups with no attached instances, IAM roles with no principals, or load balancers with no backends. - Rollback is not trivial. Downgrading Terraform CLI versions to unblock the pipeline reintroduces other 0.14 vulnerabilities and breaks provider version locking. You cannot simply pin back.
- If your team is using Terraform Cloud or Atlantis, the failed plan locks the workspace, blocking all other operators from running plans against that workspace until the lock is cleared manually.
How to Fix It (The Solution)
Basic Fix — Single Resource, No Count/For_Each
If your resource is a simple single instance with no count or for_each, the reference syntax is correct as-is. The error in this case usually means the resource block itself was renamed or removed and the reference is stale.
- description = aws_instance.web_server.id
+ description = aws_instance.web.id
Verify the resource label matches exactly:
- resource "aws_instance" "web_server" {
+ resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
}
Fix for count-Based Resources
resource "aws_instance" "app" {
- count = 3
+ count = 3
ami = var.ami_id
instance_type = "t3.micro"
}
output "instance_ids" {
- value = aws_instance.app.id
+ value = aws_instance.app[*].id
}
# When referencing a specific index:
- resource_id = aws_instance.app.id
+ resource_id = aws_instance.app[0].id
Fix for for_each-Based Resources
resource "aws_instance" "app" {
- for_each = var.instance_map
+ for_each = var.instance_map
ami = each.value.ami
instance_type = each.value.type
}
output "instance_ids" {
- value = aws_instance.app.id
+ value = { for k, v in aws_instance.app : k => v.id }
}
Enterprise Best Practice — Module Output Contracts
In shared modules, never expose raw resource references from outputs. Define explicit, typed output contracts:
# modules/compute/outputs.tf
- output "instance_id" {
- value = aws_instance.app.id
- }
+ output "instance_ids" {
+ description = "Map of logical name to EC2 instance ID for all provisioned instances."
+ value = { for k, v in aws_instance.app : k => v.id }
+ }
In the calling module:
- instance_id = module.compute.instance_id
+ instance_ids = module.compute.instance_ids
This pattern makes the for_each/count contract explicit at the module boundary and prevents consumers from accidentally referencing the collection as a scalar.
💡 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
1. terraform validate as a Pre-Commit Gate
This error is caught at validate time, not apply time. Add it to your pre-commit hooks:
# .pre-commit-config.yaml
- repo: https://github.com/antonbabenko/pre-commit-terraform
hooks:
- id: terraform_validate
- id: terraform_tflint
2. TFLint with AWS Ruleset
tflint --enable-rule=terraform_typed_variables
tflint --enable-rule=aws_instance_invalid_type
TFLint catches malformed attribute references before they hit the Terraform graph evaluator.
3. Checkov Policy for Output Validation
checkov -d . --check CKV_TF_1
For custom enforcement, write an OPA policy that asserts all module outputs referencing aws_instance use indexed or splat expressions:
# policy/instance_ref.rego
package terraform.module
deny[msg] {
output := input.configuration.root_module.outputs[_]
contains(output.expression.references[_], "aws_instance")
not contains(output.expression.references[_], "[")
msg := sprintf("Output '%v' references aws_instance without index — will fail in TF 0.15+", [output.name])
}
4. Pin Terraform Version in All Modules
# versions.tf
terraform {
required_version = ">= 0.15.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
Version pinning ensures the CI environment and developer workstations run identical evaluation semantics, eliminating "works on my machine" failures during 0.14 → 0.15 migrations.