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.Use version-controlled state backends
- 2.Run
terraform plan -refresh-onlyto see drift without changes - 3.Implement
prevent_destroyon critical resources - 4.Use
ignore_changesfor frequently changing but non-critical attributes - 5.Review provider release notes before upgrading
- 6.Use
terraform fmtandterraform validatein CI pipelines