I get a diff every time I run `terraform plan` even though no changes have been made

Hi all,

I’m fairly new to Terraform, and been having some issues with the diff I get when I run terraform plan.

Project I’m working with uses Terraform for AWS. I’ve recently created some CloudWatch alerts for various queues from SQS.

This is how my tfvars file looks like:

    stacks = ["stack1", "stack2", "stack3", "stack4"]
    queue_identifiers = ["queue1", "queue2"]
    thresholds = {
        "stack1" = {
            "queue1" = 1
            "queue2" = 1
        }
        "stack2" = {
            "queue1" = 1
            "queue2" = 1
        }
        "stack3" = {
            "queue1" = 1
            "queue2" = 1
        }
        "stack4" = {
            "queue1" = 1
            "queue2" = 1
        }
    }

Then I use these variables in conjunction to create multiple resources. Here’s my .tf file:

variable "queue_identifiers" {}
variable "stacks" {}
variable "thresholds" {}

locals {
    message_too_old_thresholds = { 
        for data in setproduct(var.stacks, var.queue_identifiers) : "message_${data[0]}_${data[1]}" => {
            stack = data[0]
            name = data[1]
            threshold = min(
                max(
                    lookup(
                        lookup(
                            var.thresholds, 
                            "${data[0]}"
                        ),
                        "${data[1]}"
                    ),
                    0
                ),
                100
            )
        }
    }
}

resource "aws_cloudwatch_metric_alarm" "message_too_old" {
  for_each            = local.message_too_old_thresholds
  alarm_name          = "${each.value.name}_message_too_old"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "ApproximateAgeOfOldestMessage"
  namespace           = "AWS/SQS"
  period              = "120"
  statistic           = "Average"
  threshold           = each.value.threshold
  alarm_description   = "Description"

  dimensions = {
    QueueName = "${each.value.stack}_${each.value.name}"
  }
}

When I run terraform plan -out tfplan this is how part of my diff looks like:

  # aws_cloudwatch_metric_alarm.message_too_old["message_stack1_queue1"] will be updated in-place
  ~ resource "aws_cloudwatch_metric_alarm" "message_too_old" {
        actions_enabled           = true
      ~ alarm_description         = "Description"
        alarm_name                = "queue1_message_too_old"
        comparison_operator       = "GreaterThanOrEqualToThreshold"
        datapoints_to_alarm       = 0
      ~ dimensions                = {
          ~ "QueueName" = "stack3_queue1" -> "stack1_queue1"
        }
        evaluation_periods        = 2
        id                        = "queue1_message_too_old"
        insufficient_data_actions = []
        metric_name               = "ApproximateAgeOfOldestMessage"
        namespace                 = "AWS/SQS"
        ok_actions                = []
        period                    = 120
        statistic                 = "Average"
        tags                      = {}
        threshold                 = 1
        treat_missing_data        = "missing"
    }

  # aws_cloudwatch_metric_alarm.message_too_old["message_stack1_queue2"] will be updated in-place
  ~ resource "aws_cloudwatch_metric_alarm" "message_too_old" {
        actions_enabled           = true
      ~ alarm_description         = "Description"
        alarm_name                = "queue2_message_too_old"
        comparison_operator       = "GreaterThanOrEqualToThreshold"
        datapoints_to_alarm       = 0
      ~ dimensions                = {
          ~ "QueueName" = "stack4_queue2" -> "stack1_queue2"
        }
        evaluation_periods        = 2
        id                        = "queue2_message_too_old"
        insufficient_data_actions = []
        metric_name               = "ApproximateAgeOfOldestMessage"
        namespace                 = "AWS/SQS"
        ok_actions                = []
        period                    = 120
        statistic                 = "Average"
        tags                      = {}
        threshold                 = 1
        treat_missing_data        = "missing"
    }

  # aws_cloudwatch_metric_alarm.message_too_old["message_stack2_queue1"] will be updated in-place
  ~ resource "aws_cloudwatch_metric_alarm" "message_too_old" {
        actions_enabled           = true
      ~ alarm_description         = "Description"
        alarm_name                = "queue1_message_too_old"
        comparison_operator       = "GreaterThanOrEqualToThreshold"
        datapoints_to_alarm       = 0
      ~ dimensions                = {
          ~ "QueueName" = "stack3_queue1" -> "stack2_queue1"
        }
        evaluation_periods        = 2
        id                        = "queue1_message_too_old"
        insufficient_data_actions = []
        metric_name               = "ApproximateAgeOfOldestMessage"
        namespace                 = "AWS/SQS"
        ok_actions                = []
        period                    = 120
        statistic                 = "Average"
        tags                      = {}
        threshold                 = 1
        treat_missing_data        = "missing"
    }

  # aws_cloudwatch_metric_alarm.message_too_old["message_stack2_queue2"] will be updated in-place
  ~ resource "aws_cloudwatch_metric_alarm" "message_too_old" {
        actions_enabled           = true
      ~ alarm_description         = "Description"
        alarm_name                = "queue2_message_too_old"
        comparison_operator       = "GreaterThanOrEqualToThreshold"
        datapoints_to_alarm       = 0
      ~ dimensions                = {
          ~ "QueueName" = "stack4_queue2" -> "stack2_queue2"
        }
        evaluation_periods        = 2
        id                        = "queue2_message_too_old"
        insufficient_data_actions = []
        metric_name               = "ApproximateAgeOfOldestMessage"
        namespace                 = "AWS/SQS"
        ok_actions                = []
        period                    = 120
        statistic                 = "Average"
        tags                      = {}
        threshold                 = 1
        treat_missing_data        = "missing"
    }
  # aws_cloudwatch_metric_alarm.message_too_old["message_stack3_queue2"] will be updated in-place
  ~ resource "aws_cloudwatch_metric_alarm" "message_too_old" {
        actions_enabled           = true
      ~ alarm_description         = "Description"
        alarm_name                = "queue2_message_too_old"
        comparison_operator       = "GreaterThanOrEqualToThreshold"
        datapoints_to_alarm       = 0
      ~ dimensions                = {
          ~ "QueueName" = "stack4_queue2" -> "stack3_queue2"
        }
        evaluation_periods        = 2
        id                        = "queue2_message_too_old"
        insufficient_data_actions = []
        metric_name               = "ApproximateAgeOfOldestMessage"
        namespace                 = "AWS/SQS"
        ok_actions                = []
        period                    = 120
        statistic                 = "Average"
        tags                      = {}
        threshold                 = 1
        treat_missing_data        = "missing"
    }

If I terraform apply the changes and run terraform plan again, same thing happens. It almost looks like setproduct is creating the result in different order every time, resulting in a weird diff.

To be clear, this doesn’t remove or add any resources, just shuffles it around, but creates a “n number of resources will be updated” warning anyhow.

Note: I’ve changed the names of my stacks and queues, and also removed some of as there are quite a lot of number of combinations. If you see anything that doesn’t make sense, please let me know.