Search code examples
azurepowershellterraform

Maps containing keys with spaces are not properly validated when those keys are used as resource names


I faced an issue when I had to terraform import some role_assignment resources, specially regarding the APP CONFIG DATA READER role assignment, in terraform.

the problem I had to solve was due to an evolution of our iac to make the terraform plan more readable and explicit.

Here is the code for role assignment that changed :

module "my_role_assignment_slot_to_app_config" {
  for_each = local.map_slot_app_config
  source = "../../resourceModules/role_assignment"
  scope = each.value.config_id
  role_definition_names = ["Reader","App Configuration Data Reader"]
  principal_id = each.value.slot_guid
}

with the following module for role assignments : 

```hcl
resource "azurerm_role_assignment" "my_role_assignment" {
  for_each              = toset(var.role_definition_names)
  scope                = var.scope
  role_definition_name = each.value
  principal_id         = var.principal_id
}

This code would plan the following, more readable : enter image description here

But as you can see, the index for the azurerm_role_assignment.my_role_assignment contains spaces.

This was preventing us to terraform import the role assignment (as it has been created manually before the iac was coded) using powershell script in an azureCli task on azure Devops :

      - task: AzureCli@2
        displayName: Runs tfImport.ps1
        condition: and(succeeded(), eq(variables['toUpdate.scriptExists'], 'true'))  # test script presence
        name: tfImport.ps1
        inputs:
          azureSubscription: ${{ parameters.serviceConnection }}
          scriptType: ps
          scriptLocation: InlineScript
          inlineScript: |
            $Env:ARM_CLIENT_ID       = "$Env:servicePrincipalId"
            $Env:ARM_CLIENT_SECRET   = "$Env:servicePrincipalKey"
            $Env:ARM_TENANT_ID       = "$Env:tenantId"
            $Env:ARM_SUBSCRIPTION_ID = az account list --query "[?isDefault].id" -o tsv        
            $Env:TF_LOG = "${{ parameters.TF_LOG }}"
        
            terraform init `
              -migrate-state `
              -backend-config="resource_group_name=${{ parameters.storageAccountResourceGroup }}"`
              -backend-config="storage_account_name=${{ parameters.storageAccount}}" `
              -backend-config="key=${{ parameters.storageContainer }}" `
              -backend-config="container_name=${{ parameters.stateBlobContainer }}"
        
            // runs my tfImport.ps1 script here
            ./tfImport.ps1 
        
          workingDirectory: $(pipeline_artefact_folder_extract)/   
          addSpnToEnvironment: true
          failOnStderr: false  
        continueOnError: true    

The script I used had the following terraform import line,

terraform import  'module.my_role_assignment_slot_to_app_config[\"sit03_z-adf-ftnd-shrd-npd-ew1-cfg01\"].azurerm_role_assignment.my_role_assignment[\"App Configuration Data Reader\"]'  /subscriptions/**********/resourceGroups/*********/providers/Microsoft.AppConfiguration/configurationStores/z-adf-ftnd-shrd-npd-ew1-cfg01/providers/Microsoft.Authorization/roleAssignments/<role_assignment_id>

and so I've had the following error :

enter image description here

(id datas removed)

After some researches, I've found the following link that explains the why and give to me one start of solution :

https://github.com/hashicorp/terraform/issues/25116

But I had to go further and find a way to use my powershell script without the startProcess method.

And as I had also to get my role_assignment resourceId from its PrincipalId (as we can gets the PrincipalId of resources that have the 'App Configuration Data Reader' role on the app_config using the following)

    # role assignment over a specific scope (such as app_config)
    $rsRolesCfg = az role assignment list  --scope /subscriptions/******/resourceGroups/*******/providers/Microsoft.AppConfiguration/configurationStores/******-cfg01 | ConvertFrom-Json
    $myRole = $rsRolesCfg | Where-Object roleDefinitionName -eq 'App Configuration Data Reader'  | Where-Object id -like "*<app_config_resourceId>"
    ## principalId is the id of the object that get the role over the scope !
    
    # GetsresourceId from PrincipalId / ObjetcId (without the '<>' on body, off course :) ) : 
    $resourceId = (az rest --method POST --url 'https://graph.microsoft.com/v1.0/directoryObjects/getByIds' --headers 'Content-Type=application/json'  --body '{\"ids\":[\"<PrincipalId>\"]}' | ConvertFrom-Json | Select-Object value).value.alternativeNames[1]

Solution from How to Get Azure AD Object by Object ID Using Azure CLI (thanks a lot !)

I had to test it locally in powershell terminal... So it did not work as expected.

So then, I change a little bit the script and got the solution (next post, as solution from my problem).


Solution

  • So I've had to test my get_ResourceId methods in local on a powershell VSCode terminal, that does not accept the above code (as the paces wher badly interpreted by powershell)

    So, after a quick search that explain to me that the "`" was the escape character for Powershell, I've tested this that works and give to me the expected resourceId for role_assignment :

    $rsRolesCfg = az role assignment list  --scope /subscriptions/************/resourceGroups/******/providers/Microsoft.AppConfiguration/configurationStores/<app_configuration_name> | ConvertFrom-Json
    
    ($rsRolesCfg | Where-Object roleDefinitionName -eq 'App Configuration Data Reader') | ForEach-Object {$local=$_.principalId; (az rest --method POST --url 'https://graph.microsoft.com/v1.0/directoryObjects/getByIds' --headers 'Content-Type=application/json'  --body "{\`"ids\`":[\`"$local\`"]}" | ConvertFrom-Json | Select-Object value).value.alternativeNames[1] }
    

    So, the use of "`" was the solution for my point, so I've tried to use it on my terraform import script (on first post) and it works fine also :

    terraform import "module.my_role_assignment_slot_to_app_config[\`"sit03_z-adf-ftnd-shrd-npd-ew1-cfg01\`"].azurerm_role_assignment.my_role_assignment[\`"App Configuration Data Reader\`"]"  /subscriptions/*****/resourceGroups/******/providers/Microsoft.AppConfiguration/configurationStores/z-adf-ftnd-shrd-npd-ew1-cfg01/providers/Microsoft.Authorization/roleAssignments/<role_assignment_id>
    

    But I had also to change the yaml task, using pscore rather than ps, like the following :

          - task: AzureCli@2
            displayName: Runs tfImport.ps1
            condition: and(succeeded(), eq(variables['toUpdate.scriptExists'], 'true'))  # test script presence
            name: tfImport.ps1
            inputs:
              azureSubscription: ${{ parameters.serviceConnection }}
              scriptType: **pscore**
    

    So then, the terrform import script was running with success !

    I've used the same "title" for this stackOverflow question / solution as the one in the GitHub, so them peoples who are lokking for that solution could find easily the solution with the question... At least, I hope so :-P

    thanks for reading !