TerraformPilot

DevOps

Fix Terraform Error - Inconsistent Plan After Apply

Fix 'inconsistent plan' errors where Terraform shows changes on every plan even after applying. Handle perpetual diffs, API normalization, and computed values.

LLuca Berton1 min read

Quick Answer

#

terraform plan shows changes every time even though you just applied. This is a perpetual diff — usually caused by the cloud API normalizing values (JSON reordering, default values) or computed attributes that change server-side. Fix with jsonencode(), ignore_changes, or provider upgrade.

The Error

#
# You just ran terraform apply, but terraform plan shows:
 
~ resource "aws_iam_role" "main" {
  ~ assume_role_policy = jsonencode(
      ~ {
          ~ Statement = [
              ~ {
                  # Whitespace/ordering differences
                }
            ]
        }
    )
  }

What Causes This

#

1. JSON Policy Normalization

#

AWS returns JSON with different key ordering or whitespace than what you sent.

2. Default Values Added by API

#

The API adds fields you didn't specify, and Terraform sees them as changes.

3. Computed Attributes

#

Values like arn, last_modified, or version change server-side.

4. Provider Bug

#

The provider doesn't properly handle read-back normalization.

How to Fix It

#

Solution 1: Use jsonencode()

#
# ❌ Raw JSON — key order may differ from API response
resource "aws_iam_role" "main" {
  assume_role_policy = <<-EOF
    {
      "Version": "2012-10-17",
      "Statement": [{"Effect": "Allow", ...}]
    }
  EOF
}
 
# ✅ jsonencode — canonical ordering
resource "aws_iam_role" "main" {
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Action    = "sts:AssumeRole"
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

Solution 2: Use aws_iam_policy_document

#
data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}
 
resource "aws_iam_role" "main" {
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

Solution 3: ignore_changes

#
resource "aws_security_group" "web" {
  name = "web-sg"
 
  lifecycle {
    ignore_changes = [
      ingress,   # API may reorder rules
      tags_all,  # Provider auto-adds default tags
    ]
  }
}

Solution 4: Upgrade Provider

#
terraform init -upgrade
terraform plan  # Check if the perpetual diff is fixed

Common Perpetual Diff Sources

#
ResourceFieldCauseFix
aws_iam_roleassume_role_policyJSON reorderjsonencode() or aws_iam_policy_document
aws_security_groupingress/egressRule reorderignore_changes or separate aws_security_group_rule
aws_lambda_functionlast_modifiedComputedignore_changes
aws_s3_bucketVariousProvider v4 migrationUpgrade to v5+
azurerm_*tagsDefault tagsignore_changes = [tags]

Troubleshooting Checklist

#
  1. ✅ Which field shows the diff? (Read the plan carefully)
  2. ✅ Is it a JSON field? → Use jsonencode() or data source
  3. ✅ Is it a computed/read-only field? → ignore_changes
  4. ✅ Is it a known provider bug? → Check GitHub issues + upgrade
#

Conclusion

#

Perpetual diffs are annoying but fixable. Use jsonencode() for IAM policies, aws_iam_policy_document data sources, ignore_changes for computed fields, and keep providers updated. Read the plan output carefully to identify which field is cycling.

#Terraform#Troubleshooting#DevOps#Error Fix#Infrastructure as Code

Share this article