Search code examples
terraformterraform-provider-awsinfrastructure-as-code

Terraform - dynamic block within resource with "count" doesn't work


I'm using the AWS provider for creating CloudWatch Metric Alarms. I created a module which takes in a variable that is a list of instance IDs, and the resource it has uses the "count" functionality to create an alarm per an Instance ID from that variable.

The "aws_cloudwatch_metric_alarm" resource can take in multiple "metric_query" blocks, and my plan was to do this as dynamic block to be able to define as many as needed in the root module.

Issue I'm experiencing is with accessing the "for_each" iterator values.

The high-level end solution should be something among these lines: Use 3 metric blocks, two are available metrics and a third one for an expression on top of the other two, and create this alarm for every instance that is provided in the instance list.

Resource definition, module code:

resource "aws_cloudwatch_metric_alarm" "alarm" {
  count               = length(var.dimension_values)
  alarm_name          = "${var.alarm_name}_${var.dimension_values[count.index]}"
  comparison_operator = var.comparison_operator
  evaluation_periods  = var.evaluation_periods
  threshold           = var.threshold
  actions_enabled     = var.actions_enabled
  alarm_actions       = var.alarm_actions

  dynamic "metric_query" {
    for_each = var.metric_queries
    content {
      id          = metric_queries.value.id
      return_data = metric_queries.value.return_data
      expression  = metric_queries.value.expression
      label       = metric_queries.value.label

      metric {
        namespace   = metric_queries.value.namespace
        metric_name = metric_queries.value.metric_name
        period      = metric_queries.value.period
        stat        = metric_queries.value.stat

        dimensions = {
          "${metric_queries.value.dimension_name}" = var.dimension_values[count.index]
        }
      }
    }
  }

  tags = merge(
    var.common_tags,
    {
      Name = "${var.alarm_name}_${var.dimension_values[count.index]}"
    }
  )
}

Module variables (only metric_queries pasted):

variable "metric_queries" {
  type = list(object({
    id             = string
    return_data    = bool
    expression     = string
    label          = string
    namespace      = string
    metric_name    = string
    period         = number
    stat           = string
    dimension_name = string
  }))
  description = "Metric query for the CloudWatch alarm"
  default     = []
}

And finally, the root module:

module "cpu_alarms" {
  source      = "../../Modules/cloudwatch_metric_alarm/"
  common_tags = local.common_tags

  # Metrics
  alarm_name          = "EC2_CPU_80_PERCENT"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 3
  threshold           = 80
  actions_enabled     = true
  alarm_actions       = ["redacted"]
  dimension_values    = local.all_ec2_instance_ids
  metric_queries = [
    {
      id = "m1"
      return_data = true
      expression = null
      label = "CPU utilization"
      namespace = "AWS/EC2"
      metric_name = "CPUUtilization"
      period = 60
      stat = "Average"
      dimension_name = "InstanceId"
    }
  ]
}

I'm getting two separate errors with this approach depending on how I'm referring to the "for_each" iterator object.

  1. When using "each" as reference to the iterator the error is: A reference to "each.value" has been used in a context in which it unavailable, such as when the configuration no longer contains the value in its "for_each" expression. Remove this reference to each.value in your configuration to work │ around this error.

  2. When using "metric_queries" as reference to the iterator the error is: A managed resource "metric_queries" "value" has not been declared in module.cpu_alarms.

What could be the root cause of this?


Solution

  • Please see the documentation on dynamic blocks. You are trying to use the syntax for the resource level for_each meta-argument, not the syntax for dynamic blocks. It's confusing that they have different syntax, but since a dynamic block could exist inside a resource with for_each, the syntax has to be different to prevent name clashes.

    For dynamic blocks the name of the variable is what you put after the dynamic key word, in your case "metric_query". So your code should look like this:

      dynamic "metric_query" {
        for_each = var.metric_queries
        content {
          id          = metric_query.value.id
          return_data = metric_query.value.return_data
          expression  = metric_query.value.expression
          label       = metric_query.value.label
    
          metric {
            namespace   = metric_query.value.namespace
            metric_name = metric_query.value.metric_name
            period      = metric_query.value.period
            stat        = metric_query.value.stat
    
            dimensions = {
              "${metric_query.value.dimension_name}" = var.dimension_values[count.index]
            }
          }
        }
      }