Fix Terraform Error: CloudWatch Log Group Already Exists
Fix terraform CloudWatch Log Group ResourceAlreadyExistsException. Import orphaned log groups, prevent Lambda auto-creation
DevOps
How to fix Terraform plan showing changes on every run caused by computed attributes, API normalization, and provider bugs. Step-by-step solutions with code.
Terraform shows changes on every plan because the cloud provider modifies values after creation (normalization), or your config uses computed/dynamic values. Use ignore_changes in a lifecycle block for provider-side normalization, jsonencode() for policy formatting issues, or explicitly set default values that match the provider's behavior.
Every time you run terraform plan, it shows changes even though nothing has actually changed:
# aws_security_group.web will be updated in-place
~ resource "aws_security_group" "web" {
~ egress {
- cidr_blocks = ["0.0.0.0/0"] -> null
- from_port = 0 -> null
}
}
Plan: 0 to add, 1 to change, 0 to destroy.# aws_iam_role.main will be updated in-place
~ resource "aws_iam_role" "main" {
~ assume_role_policy = jsonencode(
~ {
~ Statement = [
~ {
+ Sid = ""
}
]
}
)
}This isn't technically an "error" — Terraform succeeds — but it makes plan unreliable and can cause unnecessary updates in CI/CD pipelines.
Cloud providers modify values after creation. AWS might reorder JSON keys in IAM policies, add default fields, or normalize CIDR blocks. Terraform compares your config against the normalized API response and sees a "diff."
The provider automatically adds attributes you didn't specify (like default egress rules on security groups), then detects them as differences on the next plan.
Hand-written JSON in policies may have different key ordering than what the API returns:
// Your config
{"Effect": "Allow", "Action": "s3:*"}
// API returns
{"Action": "s3:*", "Effect": "Allow"}Some attributes are computed server-side and change on every read (like last_modified timestamps or computed tags).
Sometimes perpetual diffs are provider bugs — the provider incorrectly detects changes.
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
lifecycle {
ignore_changes = [
ami, # AMI updates handled separately
tags["LastUpdated"], # Timestamp tag set by Lambda
user_data, # Base64 encoding differences
]
}
}For security group egress rules that the provider adds automatically:
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = var.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
ignore_changes = [egress]
}
}Replace hand-written JSON with jsonencode() so Terraform controls the formatting:
# BAD — hand-written JSON, key order may differ from API
resource "aws_iam_role" "main" {
assume_role_policy = <<-POLICY
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
POLICY
}
# GOOD — jsonencode produces consistent output
resource "aws_iam_role" "main" {
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}Or use the IAM policy data source:
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
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
}If the provider adds default values, set them explicitly in your config:
# BAD — provider adds default egress, causing diff
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = var.vpc_id
}
# GOOD — explicitly define the default egress rule
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = var.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}For EKS clusters with tags:
resource "aws_eks_cluster" "main" {
name = "production"
role_arn = aws_iam_role.eks.arn
vpc_config {
subnet_ids = var.subnet_ids
}
# EKS adds its own tags — ignore them
lifecycle {
ignore_changes = [
tags["kubernetes.io/cluster/production"],
]
}
}If a resource should only update when specific inputs change:
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
lifecycle {
replace_triggered_by = [
var.ami_id, # Only replace when AMI changes
]
ignore_changes = [
tags,
user_data,
]
}
}Sometimes the diff is caused by stale state:
# Refresh state from the actual infrastructure
terraform apply -refresh-only
# Review the changes and approve
# This updates state without changing infrastructureIf none of the above work, it may be a provider bug:
# Check provider version
terraform version
# Upgrade to latest provider
terraform init -upgrade
terraform planSearch the provider's GitHub issues:
github.com/hashicorp/terraform-provider-aws/issuesperpetual diff or plan not empty + resource type| Resource | Attribute | Cause | Fix |
|---|---|---|---|
aws_security_group | egress | Default egress rule added by AWS | Set egress explicitly or ignore_changes |
aws_iam_role | assume_role_policy | JSON key reordering | Use jsonencode() |
aws_lambda_function | last_modified | Timestamp changes | ignore_changes = [last_modified] |
aws_autoscaling_group | desired_capacity | ASG scales independently | ignore_changes = [desired_capacity] |
aws_ecs_task_definition | container_definitions | JSON formatting | Use jsonencode() |
aws_eks_cluster | tags | Kubernetes adds tags | ignore_changes = [tags] |
azurerm_* | Various | Case sensitivity differences | Match exact casing from API |
terraform plan output)jsonencode()ignore_changesterraform apply -refresh-only resolve it?jsonencode() for all JSON — consistent formatting prevents ordering diffsignore_changes sparingly — only for genuinely external changesterraform plan in CI — catch perpetual diffs before they reach productionPerpetual diffs make terraform plan unreliable — you can't trust that "1 to change" means a real change. Fix the root cause: use jsonencode() for policies, set defaults explicitly, and use ignore_changes for attributes modified outside Terraform. A clean plan with zero changes is the goal.
Fix terraform CloudWatch Log Group ResourceAlreadyExistsException. Import orphaned log groups, prevent Lambda auto-creation
Fix terraform import errors when a resource already exists in state. Covers state rm, state show, reimport workflow, import blocks
Fix terraform too many command line arguments errors. Correct -var syntax, quote values with spaces, and learn proper Terraform CLI argument format for plan
Fix terraform invalid escape sequence errors. Double backslashes for Windows paths, use heredocs for regex, and learn all valid HCL escape sequences.