Loading...
Loading...
### Terraform Version ```shell Terraform v1.15.2 on darwin_arm64 + provider registry.terraform.io/hashicorp/aws v6.42.0 ``` ### Terraform Configuration Files ```terraform terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } provider "aws" { region = "us-east-1" } variable "enabled" { type = bool default = true } resource "aws_acm_certificate" "test" { count = var.enabled ? 1 : 0 domain_name = "tf-bug-repro.example.com" validation_method = "DNS" lifecycle { create_before_destroy = true # The precondition is the key ingredient: it makes # DiffTransformer.hasConfigConditions return true. # Any precondition or postcondition triggers the bug. precondition { condition = true error_message = "never fires" } } } ``` Nothing about the resource type is special — any managed resource will do as long as its ReadResource can return "object no longer exists." I used aws_acm_certificate because that's where I originally hit it. ### Debug Output Key excerpt from the panic: ``` [ERROR] vertex "aws_acm_certificate.test[0]" panicked panic: runtime error: invalid memory address or nil pointer dereference github.com/hashicorp/terraform/internal/terraform.(*NodeAbstractResourceInstance).readDiff internal/terraform/node_resource_abstract_instance.go:178 github.com/hashicorp/terraform/internal/terraform.(*NodeApplyableResourceInstance).managedResourceExecute internal/terraform/node_resource_apply_instance.go:219 ``` ### Expected Behavior `terraform apply` should be a no-op (or cleanly delete the orphan deposed entry from state) and exit 0. A planned `NoOp` for a deposed object that no longer exists in the remote system should not produce a crash. ### Actual Behavior `terraform apply` panics with `invalid memory address or nil pointer dereference` at `internal/terraform/node_resource_abstract_instance.go:178`. ### Steps to Reproduce The crash requires two ingredients in state-and-config that don't normally co-occur, so the reproducer constructs them deliberately: 1. `terraform init` 2. `terraform apply -auto-approve -var enabled=true` This creates aws_acm_certificate.test[0] in state and in AWS. 3. Delete the certificate out-of-band (AWS console, or `aws acm delete-certificate --certificate-arn ...`). Terraform doesn't know yet. 4. Convert the surviving current instance into a deposed-only orphan, so state has a deposed object with no current counterpart: ```shell terraform state pull > state.json cp state.json state.json.bak python3 - <<'PY' import json d = json.load(open('state.json')) for r in d['resources']: if r['type'] == 'aws_acm_certificate' and r['name'] == 'test': for inst in r['instances']: # Mark the current object as deposed instead. inst['deposed'] = 'deadbeef' d['serial'] += 1 json.dump(d, open('state.json', 'w'), indent=2) PY terraform state push state.json ``` 5. Remove the resource from config (so the only thing tying this address to anything is the deposed orphan): ```shell terraform plan -var enabled=false -out=plan.tfplan ``` The plan refresh sees the cert is gone in AWS, so the plan records a NoOp change for the deposed object. The displayed plan output reads "No changes." 6. `terraform apply plan.tfplan`. Panic. ### Additional Context The crash is at `internal/terraform/node_resource_abstract_instance.go:178`: ```go change := changes.GetResourceInstanceChange(addr, addrs.NotDeposed) log.Printf("[TRACE] readDiff: Read %s change from plan for %s", change.Action, n.Addr) ``` `GetResourceInstanceChange` is documented to return `nil` when no matching change exists; the log line dereferences `change.Action` without a nil-check. Callers downstream (e.g. `managedResourceExecute` at `node_resource_apply_instance.go:227`) all guard against `diffApply == nil`, so the nil-check appears to have been intended. Git blame: the previously-active nil-check version of `readDiff` (which `return nil, nil` on a missing change) was lying commented-out beside the live code, and was deleted in commit `2879a030` ("remove commented code", Aug 2024). The active code path had already lost the check by then. The root cause for why apply ever reaches `readDiff` with no non-deposed change is at `internal/terraform/transform_diff.go:93-99`: ```go switch rc.Action { case plans.NoOp: // For a no-op change we don't take any action but we still // run any condition checks associated with the object... update = t.hasConfigConditions(addr) ``` For a `NoOp` change attached to a **deposed** object (`rc.DeposedKey != ""`), if the resource block has any `precondition` / `postcondition`, `update` becomes `true` and the transformer creates a regular `NodeApplyableResourceInstance` for the non-deposed address (`transform_diff.go:161-186`). The deposed key isn't propagated to the update node, so when it executes it asks for the non-deposed change and finds nothing. Either: - `transform_diff.go` should not create an update node for a `NoOp` change whose `DeposedKey` is set (a deposed object has no preconditions/postconditions to evaluate), **or** - `readDiff` should handle `change == nil` the way every caller already expects. How an orphan deposed object ends up in state in the first place: in my real case, `create_before_destroy = true` left the deposed snapshot behind in two scenarios (a count→for_each migration, and a `for_each` key being removed). The cloud objects were later destroyed, leaving "deposed-only" entries in state. The repro above takes a shortcut and synthesizes the same shape directly. There is an existing test `TestContext2Plan_deposedNoLongerExists` in `internal/terraform/context_plan2_test.go:4632` that covers the same plan-side scenario but doesn't add `precondition` / `postcondition` to the resource config and doesn't proceed to apply, so it doesn't exercise this path. ### References - `TestContext2Plan_deposedNoLongerExists` — `internal/terraform/context_plan2_test.go:4632` - `DiffTransformer.Transform` — `internal/terraform/transform_diff.go:93-99, 161-186` - `NodeAbstractResourceInstance.readDiff` — `internal/terraform/node_resource_abstract_instance.go:166-181` - Commit `2879a030` ("remove commented code") — removed the commented-out nil-checking version of `readDiff` ### Generative AI / LLM assisted development? This is a true crash report that I experienced and resolved by manually deleting the orphaned deposed objects from my Terraform state with `terraform state pull` and `terraform state push`. Claude (Anthropic) assisted in localizing the crash to the source line and drafting this bug report, using my true Terraform config, state, and debug log output as context.
Click on a version to see all relevant bugs
Terraform Integration
Learn more about where this data comes from
Bug Scrub Advisor
Streamline upgrades with automated vendor bug scrubs
BugZero Enterprise
Wish you caught this bug sooner? Get proactive today.