Search code examples
terraformterraform-provider-aws

Looping over local json using for-each in Terraform


I am trying to create multiple cloudwatch alarms using input from a json file or json locally declared. Either is fine. I've removed the list of

The problem is that this is within a module, so I cannot use file.auto.tfvars.json which is how I previously did this.

File structure:

.
├── cloudwatch.tf
├── cloudwatchalarms.json
└── locals.tf

What I've tried so far:

  1. Use jsondecode to bring in the cloudwatchalarms.json and loop over it in the following way:

cloudwatch.tf

# creates alarms based on what exists within cloudwatchalarms.auto.tfvars.json
resource "aws_cloudwatch_metric_alarm" "alarms" {
  for_each            = { for alarm in local.all_alarms : alarm => alarm }
  alarm_name          = "${var.awsResourceName}-${each.value.Identifier}"
  namespace           = each.value.Namespace
  comparison_operator = each.value.ComparisonOperator
  evaluation_periods  = each.value.EvaluationPeriods
  statistic           = each.value.Statistic
  treat_missing_data  = each.value.TreatMissingData
  threshold           = each.value.Threshold
  period              = each.value.Period
  metric_name         = each.value.MetricName

  dimensions = {
    EcsServiceName = var.awsResourceName
    EcsClusterName = var.ecs_cluster_name
  }
}

locals.tf

# create locals to pull in cloudwatchalarms.json
locals {
  cloudwatchAlarms = jsondecode(file("${path.module}/cloudwatchalarms.json"))
  # loop over the cloudwatchalarms json structure
  all_alarms = [for alarms in local.cloudwatchAlarms.cloudwatchAlarms : alarms.Identifier]
}

cloudwatchalarms.json

{
        
        "cloudwatchAlarms": [
            {
                "Identifier": "ServiceMemoryUtilisation",
                "Namespace": "AWS/ECS",
                "MetricName": "MemoryUtilization",
                "Statistic": "Average",
                "Threshold": 90,
                "Period": 60,
                "EvaluationPeriods": 5,
                "ComparisonOperator": "GreaterThanThreshold",
                "TreatMissingData": "missing"
            },
            {
                "Identifier": "ServiceCPUUtilisation",
                "Namespace": "AWS/ECS",
                "MetricName": "CPUUtilization",
                "Statistic": "Average",
                "Threshold": 90,
                "Period": 60,
                "EvaluationPeriods": 5,
                "ComparisonOperator": "GreaterThanThreshold",
                "TreatMissingData": "missing"
            }
        ]      
    }
}

When using this method I get the following errors for every attribute:


Error: Unsupported attribute



  on .terraform/modules/terraform-ecs-monitoring/cloudwatch.tf line 4, in resource "aws_cloudwatch_metric_alarm" "alarms":

   4:   alarm_name          = "${var.awsResourceName}-${each.value.Identifier}"

    |----------------

    | each.value is "TaskStability"



This value does not have any attributes.
  1. The second method I tried using was declaring the json structure in locals

cloudwatch.tf

# creates alarms based on what exists within cloudwatchalarms.auto.tfvars.json
resource "aws_cloudwatch_metric_alarm" "alarms" {
  for_each            = { for alarm in local.cloudwatchAlarms : alarm.Identifier => alarm }
  alarm_name          = "${var.awsResourceName}-${each.value.Identifier}"
  namespace           = each.value.Namespace
  comparison_operator = each.value.ComparisonOperator
  evaluation_periods  = each.value.EvaluationPeriods
  statistic           = each.value.Statistic
  treat_missing_data  = each.value.TreatMissingData
  threshold           = each.value.Threshold
  period              = each.value.Period
  metric_name         = each.value.MetricName

  dimensions = {
    EcsServiceName = var.awsResourceName
    EcsClusterName = var.ecs_cluster_name
  }
}

locals.tf

locals {

    cloudwatchAlarms = {
        
        "cloudwatchAlarms": [
            {
                "Identifier": "ServiceMemoryUtilisation",
                "Namespace": "AWS/ECS",
                "MetricName": "MemoryUtilization",
                "Statistic": "Average",
                "Threshold": 90,
                "Period": 60,
                "EvaluationPeriods": 5,
                "ComparisonOperator": "GreaterThanThreshold",
                "TreatMissingData": "missing"
            },
            {
                "Identifier": "ServiceCPUUtilisation",
                "Namespace": "AWS/ECS",
                "MetricName": "CPUUtilization",
                "Statistic": "Average",
                "Threshold": 90,
                "Period": 60,
                "EvaluationPeriods": 5,
                "ComparisonOperator": "GreaterThanThreshold",
                "TreatMissingData": "missing"
            }
        ]      
    }
}

I then get this error when trying this method:


Error: Unsupported attribute



  on .terraform/modules/terraform-ecs-monitoring/cloudwatch.tf line 3, in resource "aws_cloudwatch_metric_alarm" "alarms":

   3:   for_each            = { for alarm in local.cloudwatchAlarms : alarm.Identifier => alarm }



This value does not have any attributes.



Throw to stop pipeline

Any help or pointing in the right direction would be great. Thanks.


Solution

  • You are closer to correctly constructing the set(object) in the first attempt, and restructuring the data in the second attempt. When combining the two, we arrive at:

    locals {
      cloudwatchAlarms = jsondecode(file("${path.module}/cloudwatchalarms.json"))
    }
    
    resource "aws_cloudwatch_metric_alarm" "alarms" {
      for_each            = { for alarm in local.cloudwatchAlarms : alarm.Identifier => alarm }
      ...
    }
    

    and your for_each meta-argument iterates over the set(object) (implicitly converted from the tuple type) and retrieves the value for the Identifier key in each object and the entire object. Your Map constructor in the for expression assigns the former to the key and the latter to the value, which when iterated upon will give you the desired behavior for the body of your resource block as it currently exists in the question.

    Note also that:

    alarm_name          = "${var.awsResourceName}-${each.value.Identifier}"
    

    can be simplified to:

    alarm_name          = "${var.awsResourceName}-${each.key}"