For Expression help

Hi All,

I have a problem that I’m hoping you can help with.

I use Terraform to create Azure Gallery Applications. I am trying to expand my existing code to create apps in multiple regions if required, but can’t get the code to work the way I would like! (Alternative coding options are gratefully received if you can think if an easier way to do this!)

First of all I have a yaml file that I read into Terraform with various app properties similar to below:

applications:
  application1:
    name:               "App1"
    version:            "1.0.0"
    install:               "install.bat"
    uninstall:          "uninstall.bat"
    source:             "blob url"
    region:             ["uksouth","ukwest"]
  application2:
    name:               "App2"
    version:            "1.0.1"
    install:               "install.bat"
    uninstall:          "uninstall.bat"
    source:             "blob url"
    region:             ["uksouth"]

and so on.

Within Terraform I have a local variable to read in the file:

locals {
     apps = yamldecode(file("${path.root}/config/applications_config.yaml"))
}

Later in my code, I have a for_each loop that loops through the apps to create them:

resource "azurerm_gallery_application" "this" {
    for_each                = local.apps.applications
    name                    = each.value.name
    gallery_id              = azurerm_shared_image_gallery.this.id
    location                = "uksouth"
    supported_os_type       = "Windows"
}

resource "azurerm_gallery_application_version" "this" {
  for_each                  = local.apps.applications
  name                      = each.value.version
  location                  = "uksouth"
  ...and so on (This is a long list so cut this down to keep the post short)
}

As you can see the region is hard coded, and I am now trying to make the region dynamic / automated in the code. I have added ‘region’ to the yaml as shown above, and I am now trying to create a new local variable that adds the application multiple times depending on the value of region; that way I can just for_each through the creation of all apps in all needed regions.

As an example, the local.apps.applications variable looks like this when application1 is read in from the yaml file:

{
  "application1" = {
    "install" = "install.bat"
    "name" = "App1"
    "region" = [
      "uksouth",
      "ukwest",
    ]
    "source" = "blob url"
    "uninstall" = "uninstall.bat"
    "version" = "1.0.0"
  }
}

I would like a new variable, that creates a duplicate; one for each region so it would look something like this:

{
  "application1" = {
    "install" = "install.bat"
    "name" = "App1"
    "region" = "uksouth"
    "source" = "blob url"
    "uninstall" = "uninstall.bat"
    "version" = "1.0.0"
  }
  "application2" = {
    "install" = "install.bat"
    "name" = "App1"
    "region" = "ukwest"
    "source" = "blob url"
    "uninstall" = "uninstall.bat"
    "version" = "1.0.0"
  }
}

I know a for expression is the way to go with this, but I cant figure out the expression itself.

The closest I have managed to get (and this does not work anyway) is below. I’m assuming I would need a nested for again in the empty map which brings back the multiple instances I am looking for:

tmp = {for application, appproperties in local.apps.applications : application => can(appproperties.region) ? length(appproperties.region) > 1 ? {} : appproperties : merge(appproperties, {"region" = "uksouth"})}

Thanks in Advance. Also any useful links on advanced help for for expressions would be appreciated if there is resources out there! All I can find is basic examples from Terraform help on how the function works.

Working with list comprehensions can be pretty tricky. A few things

This post might also be worth a look

This works for me as a basic POC to generate a list of anonymous objects:

locals {
  apps = yamldecode(file("${path.root}/applications_config.yaml"))
  test = flatten([
    for name, properties in local.apps.applications : [
      for region in properties.region : {
        name   = name,
        region = region,
      }
    ]
  ])
}

output "test" {
  value = local.test
}
[
  {
    "name" = "application1"
    "region" = "uksouth"
  },
  {
    "name" = "application1"
    "region" = "ukwest"
  },
  {
    "name" = "application2"
    "region" = "uksouth"
  },
]

or you could do something like

  test = [
    for name, properties in local.apps.applications : {
      for region in properties.region : "${name}-${region}" => {
        name   = name,
        region = region,
      }
    }
  ]

which would give you

[
  {
    "application1-uksouth" = {
      "name" = "application1"
      "region" = "uksouth"
    }
    "application1-ukwest" = {
      "name" = "application1"
      "region" = "ukwest"
    }
  },
  {
    "application2-uksouth" = {
      "name" = "application2"
      "region" = "uksouth"
    }
  },
]

To further flatten the rest of the properties (and, full disclosure, asked an AI tool for this part vs. playing around with getting that final merge right here), you could do something like:

  test = flatten([
    for name, properties in local.apps.applications : [
      for region in properties.region : merge(
        properties,
        {
          application = name,
          region      = region
        }
      )
    ]
  ])

Which gets you:

[
  {
    "application" = "application1"
    "install" = "install.bat"
    "name" = "App1"
    "region" = "uksouth"
    "source" = "blob url"
    "uninstall" = "uninstall.bat"
    "version" = "1.0.0"
  },
  {
    "application" = "application1"
    "install" = "install.bat"
    "name" = "App1"
    "region" = "ukwest"
    "source" = "blob url"
    "uninstall" = "uninstall.bat"
    "version" = "1.0.0"
  },
  {
    "application" = "application2"
    "install" = "install.bat"
    "name" = "App2"
    "region" = "uksouth"
    "source" = "blob url"
    "uninstall" = "uninstall.bat"
    "version" = "1.0.1"
  },
]

You could do something roughly equivalent with either a nested object or a list of objects, but this last one seems the easiest to work with to me.

Thanks for this wyardley, exactly what I needed!

I went with the last option which works well. I also wanted to include a default option if region had not been specified, so my final variable declaration code ended up like this:

locals {
    test = flatten([
        for name, properties in local.apps.applications : (
            can(properties.region) ? [
                for region in properties.region : merge(
                    properties,
                    {
                      application = "${name}_${region}",
                      region      = region
                    }
                ) 
            ] : [
                merge(
                    properties,
                    {
                        application = "${name}_uksouth",
                        region      = "uksouth"
                    }                
                )
            ]
        )
    ]) 
}

I think you could simplify to something like this then:
for region in try(properties.region, ["uksouth"])

and skip the whole separate merge.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.