Using the setproduct function in Terraform

2 minute read

Scenario

I needed a way to create all possible combinations from two lists. The first was a list of Azure AD Roles, and the second was a list of target Resource Groups where the AAD Roles should be assigned.

Solution

The solution was using Terraform’s built-in setproduct function.

The setproduct function finds all of the possible combinations of elements from all of the given sets by computing the Cartesian product.

Code Example

# Create all possible combinations from two lists, and loop through result to assign roles
# https://www.terraform.io/docs/language/functions/setproduct.html

provider "azurerm" {
  features {}
}

terraform {
  required_version = ">= 1.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.20.0"
    }
  }
}

locals {
  roles = [
    "Storage Blob Data Owner",
    "Key Vault Contributor",
  ]
  scopes = [
    "/subscriptions/SUB_NAME/resourceGroups/rg1",
    "/subscriptions/SUB_NAME/resourceGroups/rg2",
  ]

  role_scopes_product = setproduct(local.roles, local.scopes)

  # Setproduct produces a structure like this for role_scopes_product:
  # [
  #   [
  #     "Storage Blob Data Owner",
  #     "/subscriptions/SUB_NAME/resourceGroups/rg1",
  #   ],
  #   [
  #     "Storage Blob Data Owner",
  #     "/subscriptions/SUB_NAME/resourceGroups/rg2",
  #   ],
  #   [
  #     "Key Vault Contributor",
  #     "/subscriptions/SUB_NAME/resourceGroups/rg1",
  #   ],
  #   [
  #     "Key Vault Contributor",
  #     "/subscriptions/SUB_NAME/resourceGroups/rg2",
  #   ],
  # ]


  # Build a map from the above "list of lists", using a compound key of both list values, and the map value being the original list of the role and scope
  role_scopes_map_of_lists = { for role_scope in local.role_scopes_product : "${role_scope[0]}-${role_scope[1]}" => role_scope }

  # role_scopes_map_of_lists looks like this:
  # {
  #   "Key Vault Contributor-/subscriptions/SUB_NAME/resourceGroups/rg1" = [
  #     "Key Vault Contributor",
  #     "/subscriptions/SUB_NAME/resourceGroups/rg1",
  #   ]
  #   "Key Vault Contributor-/subscriptions/SUB_NAME/resourceGroups/rg2" = [
  #     "Key Vault Contributor",
  #     "/subscriptions/SUB_NAME/resourceGroups/rg2",
  #   ]
  #   "Storage Blob Data Owner-/subscriptions/SUB_NAME/resourceGroups/rg1" = [
  #     "Storage Blob Data Owner",
  #     "/subscriptions/SUB_NAME/resourceGroups/rg1",
  #   ]
  #   "Storage Blob Data Owner-/subscriptions/SUB_NAME/resourceGroups/rg2" = [
  #     "Storage Blob Data Owner",
  #     "/subscriptions/SUB_NAME/resourceGroups/rg2",
  #   ]
  # }


  role_scopes_map_of_maps = {
    for role_scope in local.role_scopes_product : "${role_scope[0]}-${role_scope[1]}" => {
        "role_name" = role_scope[0],
        "scope" = role_scope[1]
    }
  }

  # role_scopes_map_of_maps looks like this:
  # {
  #   "Key Vault Contributor-/subscriptions/SUB_NAME/resourceGroups/rg1" = {
  #     "role_name" = "Key Vault Contributor"
  #     "scope" = "/subscriptions/SUB_NAME/resourceGroups/rg1"
  #   }
  #   "Key Vault Contributor-/subscriptions/SUB_NAME/resourceGroups/rg2" = {
  #     "role_name" = "Key Vault Contributor"
  #     "scope" = "/subscriptions/SUB_NAME/resourceGroups/rg2"
  #   }
  #   "Storage Blob Data Owner-/subscriptions/SUB_NAME/resourceGroups/rg1" = {
  #     "role_name" = "Storage Blob Data Owner"
  #     "scope" = "/subscriptions/SUB_NAME/resourceGroups/rg1"
  #   }
  #   "Storage Blob Data Owner-/subscriptions/SUB_NAME/resourceGroups/rg2" = {
  #     "role_name" = "Storage Blob Data Owner"
  #     "scope" = "/subscriptions/SUB_NAME/resourceGroups/rg2"
  #   }
  # }
}

# resource groups
resource "azurerm_resource_group" "rg1" {
  name     = "rg1"
  location = "uksouth"
}

resource "azurerm_resource_group" "rg2" {
  name     = "rg2"
  location = "uksouth"
}

data "azurerm_client_config" "current" {}
data "azuread_service_principal" "current" {
  application_id = data.azurerm_client_config.current.client_id
}

# maps of lists loop example
resource "azurerm_role_assignment" "map_of_lists" {
  for_each             = local.role_scopes_map_of_lists
  scope                = each.value[1]
  role_definition_name = each.value[0]
  principal_id         = "MY_USER_ID"
}

# maps of maps loop example
resource "azurerm_role_assignment" "map_of_maps" {
  for_each             = local.role_scopes_map_of_maps
  scope                = each.value.scope
  role_definition_name = each.value.role_name
  principal_id         = data.azuread_service_principal.current.object_id
}

Code Usage

Save the code example to a local file, then run the commands below view the data structures etc:

# init
terraform init

# enter console
terraform console

# output locals to view data structures
# show all list variations
local.role_scopes_product

# show the map of lists
local.role_scopes_map_of_lists

# show the nested map
local.role_scopes_map_of_maps

# exit console

# show plan
terraform plan

Leave a comment