Creating dynamic objects in jsonencode

I’m trying to create a CloudWatch dashboard and I’m having some difficulties with trying to loop through a variable and evaluate which objects should be created inside of a jsonencode() function.

https://registry.terraform.io/providers/hashicorp/aws/5.4.0/docs/resources/cloudwatch_dashboard

Main.tf

resource "aws_cloudwatch_dashboard" "cloudwatch_dashboard" {
  dashboard_name = var.dashboard_name

  dashboard_body = jsonencode(
    {
      "start" = var.dashboard_start_time
      "end"            = var.dashboard_end_time
      "periodOverride" = var.dashboard_period_override

      "widgets" = [for item in var.widgets : {
        "type"   = lookup(item, "type")
        "x"      = lookup(item, "x", null)
        "y"      = lookup(item, "y", null)
        "width"  = lookup(item, "width", null)
        "height" = lookup(item, "height", null)

        "properties" = {
          "markdown" = lookup(item.widget_properties[0], "markdown"),
          "background" = lookup(item.widget_properties[0], "background", "solid")
        }

        "properties" = {
          "region" = lookup(item, "region", null)
          "title" = lookup(item, "title", "Default Widget Title")
          "query" = lookup(item, "query", null)
          "view" = lookup(item, "view", null)
        }
      }]
    }
  )
}

Variables.tf

variable "widgets" {
  default = [
    {
      type  = "text"
      x     = "123"
      y     = "456"
      width = "abc"
      widget_properties = [{
        markdown = "Hello"
      }]
    },
    {
      type  = "text"
      x     = "789"
      y     = "987"
      width = "def"
      widget_properties = [{
        markdown = "Hi"
      }]
    },
    {
      type  = "log"
      x     = "123"
      y     = "456"
      width = "ghi"
      widget_properties = [{
        region = "us-east-1"
        title = "test"
        query = ""
        view = ""
      }]
    }
  ]
}

What I’m trying to achieve is something like this example below where there are two widget objects defined in the JSON body. One for type = metric and another for type = text.

https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/CloudWatch-Dashboard-Body-Structure.html#CloudWatch-Dashboard-Properties-Text-Widget-Object:~:text=period%20setting%20always-,being%20obeyed.,-{ %20%20%20"start"

What I haven’t been able to figure out yet is how to only evaluate the first properties block when type = text and then only evaluate the second properties block if type = metric.

I’ve tried a few things such as directives but that doesn’t seem to work within the jsonencode function. I’ve tried using a templatefile with an interpolated jsonencode call. I’ve also tried to use a for loop with an ending if statement inside the properties block but that didn’t work either.

Is there a good way to approach this? Would I want to create the properties inside of a local block then somehow add those to the JSON body?

Hi @rk92 ,

What is the reason you want to build this complex application-specific JSON structure within Terraform expression evaluation, based on Terraform variables?

At the level of complexity you’re showcasing here, I’d be inclined to generate the JSON using non-Terraform programming techniques, save it to a file, and just load it in Terraform using a file() function.

I dare say there is a way to contrive a Terraform expression to do what you are asking about, but it is likely to be less readable, and offer less validation for incorrect input, than using non-Terraform techniques.

In addition to the direct question you asked, I notice you’re making extensive use of constructs like:

I would like to call attention to the issue that this is going to try to set view to null explicitly, when not specified - not leave out the view key entirely. Whether this will matter for CloudWatch or not, I do not know. I suspect it might cause a problem.

I was using the variable to hopefully have an easier way for end users to define their widgets rather than having them write out the JSON file completely. I did consider having a way to load in a JSON file like you mentioned as an alternative option if the user provides one.

I’ve mainly been using the lookup function since I’m looping through objects inside of lists, but I’ll try to remove the use of null as a default value to not run into any issues down the road.

In my opinion, it is easier for the users to be told “Just write a standard AWS CloudWatch dashboard definition” rather than “Write some HCL that has approximately the same keys as an AWS CloudWatch definition, but may come with some additional caveats.”.

This avoids an extra level of abstraction that you need to build and your users need to mentally translate back and forth to, especially when trying to map examples they see on the internet to your system.

I think you have an opportunity to get a better result by doing less work here than you were planning on - so I’d definitely take the opportunity!