Initializing Enclave...

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>.id inside module outputs, count-indexed resources, or cross-module wiring fail with Unsupported attribute 'id' when the reference path is structurally incorrect or the resource block is wrapped in for_each/count without proper index syntax.
  • How to fix it: Audit every aws_instance reference. If using count, use aws_instance.<name>[count.index].id. If using for_each, use aws_instance.<name>[each.key].id. If inside a module output, ensure the output block explicitly exports value = 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.tf in a shared compute module can take down 10+ environment pipelines simultaneously.
  • State drift risk: Engineers under pressure will attempt terraform apply -target workarounds. 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.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →