Search code examples
for-loopterraformevalstring-interpolationhcl

Terraform doesn't let me join a string to a variable in for loop to reference another variable


I am sure this is not a unique requirement and I have delt with this type of issue using eval() syntax in few other programming and scripting environments.

My problem: I am trying to create a azurerm_pim_active_role_assignment based on a local variable which internally is created based on a map object (group_roles). If you see the below code, I am specifically facing issue about role_definition_id = "${data.azurerm_subscription.primary.id}${local.DataFactoryContributor}" because if I apply this logic, I have to create different resource blocks for each role definition type (e.g. DataFactoryContributor) as I am not able to do something like this: eval("${data.azurerm_subscription.primary.id}${local.${group.role}). What I want to understand if it is even supported in HCL to do something like this or I will have to create different resource code blocks for each role definition?

Variable object that holds all the role and Entra group details (only 1 shared as example)

variable "groupsetuparray"{
    type = list(object({
        name       = string,
        group_name = string,
        resource_group_name = string,
        resource_name = string,
        group_roles = list(string),
        members    = list(string)
    }))
    default = [
        {
        name = "DFDeveloperTeam",
        group_name = "DFDeveloperTeam",
        resource_group_name = "",
        resource_name = "",
        group_roles = ["DataFactoryContributor","SQLServerContributor"]
        members = []
        }

Local variable to generate group roles

locals {
  group_roles = flatten([for group in var.groupsetuparray : [
      for role in group.group_roles : {
        name = group.name
        role = role
        rg = group.resource_group_name
      }
    ]
  ])
DataFactoryContributor = data.azurerm_role_definition.DataFactoryContributor.id
SQLServerContributor = data.azurerm_role_definition.SQLServerContributor.id
}

data resources to reference the inbuilt role definitions

data "azurerm_role_definition" "DataFactoryContributor" {
  name = "Data Factory Contributor"
}

data "azurerm_role_definition" "SQLServerContributor" {
  name = "SQL Server Contributor"
}

Resource for creating PIM activation role assignment using for each loop

resource "azurerm_pim_active_role_assignment" "DataFactoryContributorAssignment" {
  for_each = { for group in local.group_roles : "${group.name}-${group.role}" => group if group.role == "DataFactoryContributor" }
  scope              = data.azurerm_subscription.primary.id
  role_definition_id = "${data.azurerm_subscription.primary.id}${group.role}.id"
#=====BELOW IS CODE THAT WORKS BUT I HAVE TO GIVE DIRECT REFERENCE FOR EACH ROLE DEFINITION, WHICH IS NOT BEST WAY=====
#role_definition_id = "${data.azurerm_subscription.primary.id}${local.DataFactoryContributor}"
#=====
  principal_id       = azuread_group.group[each.value.name].object_id
  schedule {
    start_date_time = time_static.example.rfc3339
    expiration {
      duration_hours = 8
    }
  }
  justification = "Expiration Duration Set"
    ticket {
    number = "1"
    system = "example ticket system"
  }

}

Error message that I am getting if I used above code block

Error: Reference to undeclared resource
│
│   on main.tf line 61, in resource "azurerm_pim_active_role_assignment" "DataFactoryContributor":
│   61:   role_definition_id = "${data.azurerm_subscription.primary.id}${group.role}.id"
│
│ A managed resource "group" "role" has not been declared in the root module.

I am still learning TF HCL (as you might feel after looking at above code) so it will be great help if someone can please point me in correct direction.

What I have already tried: I tried to use lookup function, but that doesn't work. Also I have tried different ways of expressions and interpolation but none worked.


Solution

  • Below is a small example using for_each with your data...

    I've done a few changes to your code:

    • reduce your default input to just a couple of attributes in the object
    • use type = any just to make the code smaller
    • and to show the loop I'm using a null_resource with some triggers
    variable "groupsetuparray" {
      type = any
      default = [
        {
          name        = "DFDeveloperTeam",
          group_roles = ["DataFactoryContributor", "SQLServerContributor"]
        }
      ]
    }
    
    locals {
      group_roles = flatten([
        for group in var.groupsetuparray : [
          for role in group.group_roles : {
            name = group.name
            role = role
          }
        ]
      ])
    }
    
    resource "null_resource" "test" {
      for_each = {
        for group in local.group_roles :
        "${group.name}-${group.role}" => group
        if group.role == "DataFactoryContributor"
      }
      triggers = {
        "key"  = each.key
        "role" = each.value.role
      }
    }
    

    a terraform plan on that will look like:

    Terraform will perform the following actions:
    
      # null_resource.test["DFDeveloperTeam-DataFactoryContributor"] will be created
      + resource "null_resource" "test" {
          + id       = (known after apply)
          + triggers = {
              + "key"  = "DFDeveloperTeam-DataFactoryContributor"
              + "role" = "DataFactoryContributor"
            }
        }
    

    The two data you can refactor into a loop as well

    FROM

    data "azurerm_role_definition" "DataFactoryContributor" {
      name = "Data Factory Contributor"
    }
    
    data "azurerm_role_definition" "SQLServerContributor" {
      name = "SQL Server Contributor"
    }
    

    TO

    data "azurerm_role_definition" "roles" {
      for_each = toset(["SQL Server Contributor", "Data Factory Contributor"])
      name     = each.value
    }
    

    then you can access them with the key:

     data.azurerm_role_definition.roles["SQL Server Contributor"]
    

    Here is another example slightly more complex...
    using a data "local_file" with a loop:

    variable "groupsetuparray" {
      type = any
      default = [{
        name        = "DFDeveloperTeam",
        group_roles = ["DataFactoryContributor", "SQLServerContributor"]
      }]
    }
    
    locals {
      group_roles = flatten([
        for group in var.groupsetuparray : [
          for role in group.group_roles : {
            name = group.name
            role = role
          }
        ]
      ])
    }
    
    data "local_file" "input" {
      for_each = toset(["DataFactoryContributor", "SQLServerContributor"])
      filename = "${path.module}/${each.value}.txt"
    }
    
    resource "null_resource" "test" {
      for_each = {
        for group in local.group_roles :
        "${group.name}-${group.role}" => group
      }
      triggers = {
        "key"     = each.key
        "role"    = each.value.role
        "content" = data.local_file.input[each.value.role].content
      }
    }
    

    I have this full code in Github:
    https://github.com/heldersepu/hs-scripts/tree/master/TerraForm/group_roles