RAPID INCIDENT TRIAGE & ACTION PLAN•Syntax
01. Error Signature
Terraform State MV Resource Discovery Fix
02. Root Cause Vector

terraform state mv fails with 'Resource not found' when the source address doesn't match the actual state key after a manual infrastructu...

03. Rapid Prescription

Refresh remote state lock, ensure provider version constraints, and audit state drift.

Zero-Trust Client IDE/ Syntax

Terraform State MV Resource Discovery Fix

terraform state mv fails with 'Resource not found' when the source address doesn't match the actual state key after a manual infrastructure change.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(63L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# TODO: This module is a mess, but it works in prod. Don't touch unless you have to.
# Originally for the pci-secure-zone, but we had to adapt it for the legacy-api in my-registry.internal.
# VULNERABILITY: Manual resource rename in state without updating module source creates a hidden mismatch.

module "legacy_database" {
  source = "git::https://my-registry.internal/legacy-api/terraform-modules.git//database?ref=v1.2.3"

  # Old naming from the PCI zone, kept for backward compatibility
  identifier = "prod-legacy-db-${var.environment}"
  # TODO: fix this workaround for multi-region failover
  # engine_version = "11.2"
  engine_version = "12.4"

  # Secrets injected via env in CI, but fallback for local dev (remove before commit!)
  master_password = "SuperSecretDBPass123!@#"
  # AWS keys for cross-account backup (rotated quarterly)
  backup_role_arn = "arn:aws:iam::123456789012:role/backup-role"
  aws_access_key = "AKIAIOSFODNN7EXAMPLE"
  aws_secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

# This was the old module call before the refactor. Keeping it commented for reference.
# module "database_old" {
#   source = "git::https://my-registry.internal/terraform-modules.git//database?ref=v0.9.1"
#   identifier = "prod-db-${var.environment}"
# }

# New Kafka cluster config (WIP - not fully integrated)
resource "aws_msk_cluster" "legacy_kafka" {
  cluster_name = "legacy-kafka-${var.environment}"

  # Stripe API token for billing integration (test mode)
  client_authentication {
    sasl {
      scram = true
      # TODO: Move this to Secrets Manager
      # VULNERABILITY: Manual 'terraform state mv' from 'module.legacy_database.aws_db_instance.primary' to 'module.database_old.aws_db_instance.primary' will fail here due to source mismatch.
      # The state expects the old module source, but the config references the new one implicitly via dependencies.
      # This creates a silent drift until 'terraform apply' or 'state mv' is attempted.
      # Stripe test key below - DO NOT commit to version control
      secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:stripe-test-key-abc123"
      stripe_api_token = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"
    }
  }

  # Multi-line RSA key for SSH tunneling (bastion host access)
  encryption_info {
    encryption_at_rest_kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
    # TODO: Rotate this key - it's been here since 2020
    # -----BEGIN RSA PRIVATE KEY-----
    # MIIEowIBAAKCAQEAz7J8GpU6p7KzJ8GpU6p7KzJ8GpU6p7KzJ8GpU6p7KzJ8GpU6p7K
    # ... (truncated for brevity) ...
    # -----END RSA PRIVATE KEY-----
    ssh_private_key = "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAz7J8GpU6p7KzJ8GpU6p7KzJ8GpU6p7KzJ8GpU6p7KzJ8GpU6p7K\n...\n-----END RSA PRIVATE KEY-----"
  }
}

# Database URI for the legacy app (hardcoded due to time constraints)
output "database_endpoint" {
  value = "postgres://user:${module.legacy_database.master_password}@${module.legacy_database.endpoint}:5432/legacy_db"
  sensitive = true
}
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix 'terraform state mv' Resource Not Found Error After Manual Infrastructure Changes

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins


TL;DR

  • What broke: terraform state mv cannot locate the source resource because the address you supplied does not match the key Terraform actually stored in state — typically caused by a manual resource rename, module refactor, or provider upgrade that changed the resource type string.
  • How to fix it: Run terraform state list to dump every real address in state, find the correct source key, then re-run terraform state mv <correct_source> <destination>.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your terraform.tfstate or terraform state list output and get the corrected state mv commands generated locally without sending your state file to any external server.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

You ran something like:

$ terraform state mv 'aws_instance.web' 'module.compute.aws_instance.web'

And got hit with:

Error: Invalid target address

The given source address does not match any resource instance in the state.

Resource not found: aws_instance.web

Or in older Terraform versions (< 1.x):

Failed to move resource: Resource 'aws_instance.web' not found in state.

Immediate consequence: The state move does not execute. Your pending refactor — moving a resource into a module, renaming it, or migrating to a new provider resource type — is blocked. If you already removed the original resource block from your .tf files, your next terraform plan will show a destructive destroy for the live infrastructure. You are one terraform apply away from deleting production resources.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is not a security exploit vector, but the blast radius is severe:

  1. Unintended destroy-then-recreate: Terraform sees the resource missing from state but present in config (or vice versa). It plans a destroy + create. For stateful resources — RDS instances, EBS volumes, Elasticache clusters — this means data loss or multi-hour downtime.

  2. State drift compounds the problem: If the manual change (console rename, AWS tag update, provider upgrade) also mutated the resource ID stored in state, a blind state mv with the wrong address silently corrupts state by creating a duplicate or orphaned entry.

  3. Team blast radius in remote state: With S3+DynamoDB or Terraform Cloud as the backend, a botched state operation affects every engineer on the team simultaneously. State lock doesn't protect against writing garbage — it only prevents concurrent writes.

  4. Root causes that produce this error:

    • Module restructure: Resource moved into or out of a module changes its address from aws_instance.web to module.compute.aws_instance.web.
    • count or for_each migration: Address changes from aws_instance.web to aws_instance.web[0] or aws_instance.web["prod"].
    • Provider upgrade: Resource type renamed (e.g., aws_elasticache_cluster → aws_elasticache_replication_group semantics shift, or hashicorp/aws 3.x → 4.x type splits).
    • Manual state edit gone wrong: Someone ran terraform state rm or edited the JSON directly and the key is now gone or malformed.
    • Workspace mismatch: You are in workspace default but the resource lives in workspace prod.

How to Fix It (The Solution)

Step 0: Back up state before touching anything

# Remote backend — pull a local copy first
terraform state pull > terraform.tfstate.backup.$(date +%Y%m%d%H%M%S)

Do not skip this. State corruption without a backup is unrecoverable.


Step 1: Discover the real address

# List every resource address currently in state
terraform state list

# If you suspect a module path or index issue, grep for partial name
terraform state list | grep -i 'web'

# Inspect the full attributes of a candidate resource
terraform state show 'module.compute.aws_instance.web["prod"]'

The output of terraform state list gives you the exact string you must use as the source address in state mv.


Basic Fix

Once you have the correct source address from state list:

- terraform state mv 'aws_instance.web' 'module.compute.aws_instance.web'
+ terraform state mv 'module.legacy.aws_instance.web' 'module.compute.aws_instance.web'

For a count-indexed resource:

- terraform state mv 'aws_instance.web' 'aws_instance.web[0]'
+ # First confirm the exact index in state:
+ # terraform state list | grep aws_instance.web
+ terraform state mv 'aws_instance.web' 'aws_instance.web[0]'

(The command may be identical — the fix is confirming the source address matches exactly what state list returns before running it.)

For a for_each migration from count:

- terraform state mv 'aws_instance.web[0]' 'aws_instance.web["prod"]'
+ terraform state mv 'aws_instance.web[0]' 'aws_instance.web["prod"]'
# Verify with: terraform state show 'aws_instance.web["prod"]' after the move

Enterprise Best Practice: Scripted Bulk State Moves with Validation

For module refactors affecting dozens of resources, never run state mv commands manually one-by-one. Use a validated script:

- # Manual, error-prone one-liner with hardcoded addresses
- terraform state mv 'aws_instance.web' 'module.compute.aws_instance.web'
- terraform state mv 'aws_security_group.web' 'module.compute.aws_security_group.web'

+ #!/usr/bin/env bash
+ # enterprise_state_mv.sh — validate before move, log every operation
+ set -euo pipefail
+
+ BACKUP="terraform.tfstate.backup.$(date +%Y%m%d%H%M%S)"
+ echo "[INFO] Pulling state backup to ${BACKUP}"
+ terraform state pull > "${BACKUP}"
+
+ declare -A MOVES=(
+   ["aws_instance.web"]="module.compute.aws_instance.web"
+   ["aws_security_group.web"]="module.compute.aws_security_group.web"
+ )
+
+ for src in "${!MOVES[@]}"; do
+   dst="${MOVES[$src]}"
+   # Validate source exists before attempting move
+   if terraform state list | grep -qxF "${src}"; then
+     echo "[MOVE] ${src} -> ${dst}"
+     terraform state mv "${src}" "${dst}"
+   else
+     echo "[SKIP] Source not found in state: ${src}" >&2
+   fi
+ done
+
+ echo "[INFO] Post-move state list:"
+ terraform state list

For Terraform 1.1+, prefer moved blocks in HCL over imperative state mv — they are version-controlled, reviewable, and automatically applied during plan/apply:

- # Imperative, untracked, not in version control
- terraform state mv 'aws_instance.web' 'module.compute.aws_instance.web'

+ # In versions.tf or a dedicated moved.tf — tracked in Git, applied automatically
+ moved {
+   from = aws_instance.web
+   to   = module.compute.aws_instance.web
+ }

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Enforce `moved` blocks over imperative `state mv` in code review

Add a PR checklist item: "Any resource rename/module move must use a moved {} block, not a manual state mv command." moved blocks are auditable, reversible (remove the block after one apply cycle), and prevent this entire class of error.

2. Checkov / tfsec scan for state hygiene signals

While Checkov doesn't directly lint state addresses, add a pre-plan script in CI:

# .github/workflows/terraform.yml
- name: Validate state addresses match config
  run: |
    terraform init -backend=false
    # Fail if any resource in state has no corresponding config block
    # (signals a missing moved{} block or accidental state rm)
    terraform plan -detailed-exitcode 2>&1 | grep -E '^  # .* will be destroyed' && \
      echo '::error::Destroy detected — verify moved{} blocks are present' && exit 1 || true

3. OPA / Sentinel policy: block apply if destroy count exceeds threshold

# sentinel/no-mass-destroy.sentinel
import "tfplan/v2" as tfplan

max_destroys = 2

destroy_count = length(filter tfplan.resource_changes as _, rc {
  rc.change.actions contains "delete"
})

main = rule {
  destroy_count <= max_destroys
}

This gates terraform apply in Terraform Cloud/Enterprise if a botched state move causes unexpected destroys.

4. Lock state backend and require MFA for state operations

# In your S3 backend config
terraform {
  backend "s3" {
    bucket         = "my-tf-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-state-lock"
    encrypt        = true
    # Require MFA delete on the S3 bucket at the bucket policy level
    # to prevent accidental state wipes
  }
}

5. `terraform state list` as a pre-flight check in every pipeline

Make terraform state list | wc -l a pipeline step that outputs resource count and fails if count drops by more than N% between runs — a cheap canary for accidental state truncation.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does 'terraform state mv' say 'Resource not found' even though the resource exists in my AWS console?
The error is a state desync, not a cloud resource issue. Terraform's state file is a local, opaque database mapping your code's resource addresses (like 'module.compute.aws_instance.web["prod"]') to real cloud IDs. When you manually alter infrastructure or code, this mapping breaks. The address you're using no longer matches the cryptographic key in the state file. Running 'terraform state list' reveals the true, often obfuscated, address—a fragile, manual discovery process that exposes your entire state file to human error.
Q2.Should I use 'terraform state mv' or a 'moved {}' block to refactor resources into modules?
Both are manual traps. 'moved {}' blocks require perfect foresight during the refactor and still rely on a successful 'terraform apply'—which can fail mid-operation, leaving state partially corrupted. A manual 'state mv' is worse: one typo can orphan critical resources, creating unmanaged attack surfaces and compliance gaps. Every second you spend manually crafting these commands, your security posture decays. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix with a cryptographically verified execution path.
Q3.How do I safely perform a bulk state move for a large module restructure without corrupting state?
You can't. The 'backup and script' approach is a compliance nightmare. It lacks audit trails, cannot be integrated into GitOps pipelines, and any script error scales the blast radius across your entire mono-repo. Manual 'terraform plan' verification is fallible and doesn't prevent state corruption during the apply phase. For enterprise-scale remediation, upgrade to the StackEngine CI/CD Automator (Team Edition). It provides on-prem, automated pull request generation with Maker-Checker review with policy-enforced rollbacks, full audit logging for SOC2, and eliminates manual state manipulation forever.