Running terraform plan and seeing unexpected changes can be alarming, especially in production environments. Understanding why Terraform detects changes helps prevent unwanted modifications.

Understanding the Problem

Unexpected plan changes fall into categories:

``` # aws_instance.web will be updated in-place ~ resource "aws_instance" "web" { ~ user_data = "abc123" -> "def456" # Unexpected }

# aws_s3_bucket.logs will be replaced -/+ resource "aws_s3_bucket" "logs" { ~ bucket = "logs-old" -> "logs-new" # Forces replacement }

# aws_instance.legacy will be destroyed - resource "aws_instance" "legacy" { # Unexpected destruction } ```

Cause 1: Drift from Manual Changes

Manual changes made outside Terraform cause drift.

Detection: ``bash terraform plan -out=tfplan terraform show -json tfplan | jq '.resource_changes[] | select(.change.actions != ["no-op"])'

Solution:

Import existing resources or revert manual changes: ```bash # Import manually created resource terraform import aws_instance.manual i-1234567890abcdef0

# Or update Terraform config to match reality terraform apply -refresh-only # Updates state to match real world ```

Prevent drift by using policies that prevent console/API modifications.

Cause 2: Provider Defaults Changing

Provider updates can change default values.

Error Example: `` ~ resource "aws_db_instance" "main" { ~ performance_insights_enabled = false -> true }

Solution:

Explicitly set values to prevent drift: ```hcl resource "aws_db_instance" "main" { # Explicitly set to prevent provider defaults from changing performance_insights_enabled = false

# Or use lifecycle to ignore certain changes lifecycle { ignore_changes = [performance_insights_enabled] } } ```

Cause 3: Computed Values Causing Updates

Dynamic values that change on each run cause perpetual updates.

Error Example: `` ~ resource "aws_lambda_function" "api" { ~ source_code_hash = "abc123=" -> "def456=" }

Solution:

Use keepers or lifecycle rules: ```hcl resource "aws_lambda_function" "api" { filename = "function.zip" source_code_hash = filebase64sha256("function.zip")

lifecycle { # Prevent updates when only hash changes due to timestamps ignore_changes = [source_code_hash] } }

# Better approach: use null_resource with triggers resource "null_resource" "lambda_build" { triggers = { source_hash = filebase64sha256("function.zip") }

provisioner "local-exec" { command = "zip function.zip lambda.py" } } ```

Cause 4: Ordering and List Changes

List order changes trigger unnecessary updates.

Error Example: `` ~ resource "aws_security_group" "app" { ~ ingress = [ - { port = 443, protocol = "tcp" }, { port = 80, protocol = "tcp" }, + { port = 443, protocol = "tcp" }, ] }

Solution:

Use stable ordering or ignore order: ```hcl resource "aws_security_group" "app" { # Sort rules consistently ingress = [ { port = 80, protocol = "tcp", ... }, { port = 443, protocol = "tcp", ... }, ]

lifecycle { create_before_destroy = true } } ```

Cause 5: Forces Replacement Issues

Some attribute changes force resource replacement when you want in-place updates.

Error Example: `` -/+ resource "aws_instance" "web" (new resource required) ~ ami = "ami-abc123" -> "ami-def456" # forces replacement

Solution:

Use create_before_destroy for zero-downtime updates: ```hcl resource "aws_instance" "web" { ami = var.ami_id instance_type = "t3.micro"

lifecycle { create_before_destroy = true } }

# Or prevent replacement entirely resource "aws_instance" "web" { lifecycle { prevent_destroy = true } } ```

Cause 6: State File Corruption

Corrupted state causes phantom changes.

Detection: ``bash terraform state list terraform state show aws_instance.web

Solution:

Refresh state from actual resources: ```bash # Refresh state without making changes terraform refresh

# Or re-import if severely corrupted terraform state rm aws_instance.web terraform import aws_instance.web i-1234567890abcdef0 ```

Advanced Debugging

Enable detailed logging: ``bash export TF_LOG=DEBUG terraform plan 2>&1 | tee terraform-debug.log

Compare states: ``bash # Show what changed between two states terraform state pull > current.tfstate # Make changes terraform plan terraform state pull > new.tfstate diff current.tfstate new.tfstate

Use the plan JSON for analysis: ``bash terraform plan -out=tfplan terraform show -json tfplan > plan.json jq '.resource_changes[] | select(.change.actions | index("update") or index("delete"))' plan.json

Verification Steps

After resolving, verify the plan shows no unexpected changes: ``bash terraform plan # Should show: No changes. Your infrastructure matches the configuration.

Run targeted plans to check specific resources: ``bash terraform plan -target=aws_instance.web

Prevention Best Practices

  1. 1.Use version-controlled state backends
  2. 2.Run terraform plan -refresh-only to see drift without changes
  3. 3.Implement prevent_destroy on critical resources
  4. 4.Use ignore_changes for frequently changing but non-critical attributes
  5. 5.Review provider release notes before upgrading
  6. 6.Use terraform fmt and terraform validate in CI pipelines