Search code examples
azureazure-resource-managerazure-bicep

Azure pipeline iteration for ARM template Parameters


I have a bicep file as below to create role assignment to resourceGroup scope using azure devops pipeline.

main.bicep

targetScope = 'resourceGroup'

@description('Principal type of the assignee.')
@allowed([
  'Device'
  'ForeignGroup'
  'Group'
  'ServicePrincipal'
  'User'
])
param principalType string

@description('the id for the role defintion, to define what permission should be assigned')
param RoleDefinitionId string

@description('the id of the principal that would get the permission')
param principalId string

@description('the role deffinition is collected')
resource roleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  scope: subscription()
  name: RoleDefinitionId
}

resource RoleAssignment 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = {
  name: guid(resourceGroup().id, RoleDefinitionId, principalId)
  properties: {
    roleDefinitionId: roleDefinition.id
    principalId: principalId
    principalType: principalType
  }
}

This is my pipeline where I want to build the bicep and pass multiple principle iD as array.but its failing

pipeline.yaml.

parameters:
- name: roleList
  type: object

stages:
- stage: BuilD_Roles_ARM_Artifact
  displayName: 'Build_ARM_Template'           
  jobs:
  - ${{ each role in parameters.roleList }}:
    - job: BuilD_ARM_Artifact_${{ role.environment }}_${{ role.rolesname }}
      displayName: '${{ role.rolesname }}'
      variables:
      - name: subscription
        ${{ if or(eq(role.environment, 'development'), eq(role.environment, 'staging')) }}:          
          value: 'mynonprod'
        ${{ if eq(role.environment, 'production')}}:
          value: "myprod"
        ${{ if eq(role.environment, 'dr')}}:
          value: "mydr"          
      workspace:
        clean: all
      pool:
        ${{ if eq(role.environment, 'development')}}:
          name: devpool
        ${{ if eq(role.environment, 'staging')}}:
          name: stagepool
        ${{ if eq(role.environment, 'production')}}:
          name: az-prod-spoke
        ${{ if eq(role.environment, 'dr')}}:
          name: drpool     
      steps:
      - bash: |      
          resourceGroup=${{ role.resourceGroup }}
          echo "##vso[task.setvariable variable=resourceGroup]$resourceGroup"
          principalType=${{ role.principalType }}
          echo "##vso[task.setvariable variable=principalType]$principalType"
          principalid=${{ role.principalid }}
          echo "##vso[task.setvariable variable=principalid]$principalid"
          roleDefinitionId=${{ role.roleDefinitionId }}
          echo "##vso[task.setvariable variable=roleDefinitionId]$roleDefinitionId"
      - bash: az bicep build --file template/main.bicep
        displayName: 'Compile Bicep to ARM'
      - task: qetza.replacetokens.replacetokens-task.replacetokens@3
        inputs:
          rootDirectory: '$(System.DefaultWorkingDirectory)/'
          targetFiles: '$(System.DefaultWorkingDirectory)/template/parameters.json'
          encoding: 'auto'
          writeBOM: true
          actionOnMissing: 'warn'
          keepToken: false
          tokenPrefix: '#{'
          tokenSuffix: '}#'
          useLegacyPattern: false
          enableTelemetry: true
      - task: AzureCLI@2
        displayName: "validate the templates"
        inputs:
          azureSubscription: ${{ variables.subscription }}
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: 'az group deployment validate --resource-group $(resourceGroup) --template-file $(System.DefaultWorkingDirectory)/template/main.json --parameters $(System.DefaultWorkingDirectory)/template/parameters.json'
      - task: AzureCLI@2
        displayName: "verify the change result"
        inputs:
          azureSubscription: ${{ variables.subscription }}
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: 'az deployment group what-if --resource-group $(resourceGroup) --template-file $(System.DefaultWorkingDirectory)/template/main.json --parameters $(System.DefaultWorkingDirectory)/template/parameters.json'                
      - task: PublishBuildArtifacts@1
        inputs:
          PathtoPublish: '$(System.DefaultWorkingDirectory)/template/'
          ArtifactName: 'arm-source-${{ role.environment }}-${{ role.rolesname }}'
          publishLocation: 'Container'
        name: "Publish_arm_code"
        displayName: "Publish arm code as build artifact"
    
- stage: Create_RoleAssignment
  displayName: 'Create RoleAssignment'                 
  jobs:
  - ${{ each role in parameters.roleList }}:        
    - deployment: deploy_role_${{ role.environment }}_${{ role.rolesname }}
      displayName: '${{ role.rolesname }}'
      variables:
      - name: resourceGroup
        value: ${{ role.resourceGroup }}          
      - name: subscription
        ${{ if or(eq(role.environment, 'development'), eq(role.environment, 'staging')) }}:          
          value: 'mynonprod'
        ${{ if eq(role.environment, 'production')}}:
          value: "myprod"
        ${{ if eq(role.environment, 'dr')}}:
          value: "mydr"
      ${{ if eq(variables.subscription, 'mynonprod') }}:
        environment: NON-PROD-RBAC      
      ${{ if eq(variables.subscription, 'myprod') }}:
        environment: PROD-RBAC
      ${{ if eq(variables.subscription, 'mydr') }}:
        environment: DR-RBAC
      pool:
        ${{ if eq(variables.subscription, 'mynonprod') }}:          
          name: devpool
        ${{ if eq(variables.subscription, 'mytest') }}:
          name: stagepool
        ${{ if eq(variables.subscription, 'myprod') }}:
          name: az-prod-spoke
        ${{ if eq(variables.subscription, 'mydr') }}:
          name: drpool                 
      strategy:
        runOnce:
          deploy:
            steps:
            - download: none
            - task: DownloadBuildArtifacts@0
              inputs:
                artifactName: 'arm-source-${{ role.environment }}-${{ role.rolesname }}'
                downloadPath: $(System.ArtifactsDirectory)              
            - task: CopyFiles@2
              inputs:
                sourceFolder: $(System.ArtifactsDirectory)/arm-source-${{ role.environment }}-${{ role.rolesname }}
                contents: '**'
                targetFolder: $(System.DefaultWorkingDirectory)/arm-source-${{ role.environment }}-${{ role.rolesname }}
                cleanTargetFolder: true
            - task: AzureCLI@2
              displayName: "Create the change result"
              inputs:
                azureSubscription: ${{ variables.subscription }}
                scriptType: 'bash'
                scriptLocation: 'inlineScript'
                inlineScript: 'az deployment group create --resource-group $(resourceGroup) --template-file $(System.DefaultWorkingDirectory)/arm-source-${{ role.environment }}-${{ role.rolesname }}/main.json --parameters $(System.DefaultWorkingDirectory)/arm-source-${{ role.environment }}-${{ role.rolesname }}/parameters.json'

and this is my pipeline input file

name: $(Build.SourceBranchName)-$(Build.BuildId)
trigger: none

stages:
- template: azure-pipeline.yaml
  parameters:
    roleList:
    - rolesname: rolename1
      environment: development
      scope: resourcegroup
      principalType: Group     
      principalid: xxxxxxxxxxx,yyyyyyyy, zzzzzzzzz
      roleDefinitionId: acdxxxxxxxxxxxxxxxxxxxxx    # reader id
      resourceGroup: myrg-1

    - rolesname: rolename2
      environment: development      
      scope: resourcegroup
      principalType: Group     
      principalid: aaaaaaaa,bbbbbbbbbb,cccccccccc         
      roleDefinitionId: acdxxxxxxxxxxxxxxxxxxxxx    # reader id
      resourceGroup: myrg-2

    - rolesname: rolename3
      environment: development      
      scope: resourcegroup
      principalType: Group     
      principalid:          
      roleDefinitionId: acdxxxxxxxxxxxxxxxxxxxxx    # reader id
      resourceGroup: myrg-3

so here first I am building the bicep to ARM file and replacing the variables with the pipelines variables over a loop, Which is creating mutiple ARM templates together.

So I am looking for 2 things.

  1. I would be able to pass list of PrincipleIDs over the input, for each roleassignment. The above template syntax is failing if I add multiple PrincipleIDs

  2. looking for a way to iterate through all the roles paramaters i9f any changes added and have a single ARM template with the inputs. So that it will produce a single ARM build artifact for only the modification to roleassignment items.

Adding the modified files

New bicep file

targetScope = 'resourceGroup' 

@description('Principal type of the assignee.')
@allowed([
  'Device'
  'ForeignGroup'
  'Group'
  'ServicePrincipal'
  'User'
])
param principalType string

@description('the id for the role defintion, to define what permission should be assigned')
param RoleDefinitionId string

@description('the id of the principal that would get the permission')
param principalId string

@description('the role deffinition is collected')
resource roleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  scope: subscription()
  name: RoleDefinitionId
}

resource RoleAssignment 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = [for id in split(principalId, ','): {
  name: guid(resourceGroup().id, RoleDefinitionId, principalId)
  properties: {
    roleDefinitionId: roleDefinition.id
    principalId: principalId
    principalType: principalType
  }
}]

parameters file

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
     "principalType": {
         "value": "#{principalType}#"
     },
     "RoleDefinitionId": {
       "value": "#{RoleDefinitionId}#"          
     },     
     "principalId": {
       "value": "#{principalId}#"
     }
  } 
}

pipeline file.

  - rolesname: readerall
    environment: development      
    scope: resourcegroup
    principalType: Group     
    principalid: aaaaaaaaaaaaaa,bbbbbbbbbbbbbbbbb,ccccccccccccccccccccc,ddddddddddddddddddddd
    roleDefinitionId: acddxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    resourceGroup: aks-rg

  - rolesname: reader_apimrg_all
    environment: development      
    scope: resourcegroup
    principalType: Group     
    principalid: aaaaaaaaaaaaaa,bbbbbbbbbbbbbbbbb,ccccccccccccccccccccc,ddddddddddddddddddddd
    roleDefinitionId: acddxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    resourceGroup: apim-rg


  - rolesname: reader_lawrg_all
    environment: development      
    scope: resourcegroup
    principalType: Group     
    principalid: aaaaaaaaaaaaaa,bbbbbbbbbbbbbbbbb,ccccccccccccccccccccc,ddddddddddddddddddddd
    roleDefinitionId: acddxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    resourceGroup: la-rg

Solution

  • Here the principal ids are passed as string:

    principalid: xxxxxxxxxxx,yyyyyyyy,zzzzzzzzz
    

    In the bicep file, you could split the string to create multiple role assignments:

    resource roleAssignments 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = [for id in split(principalId, ','): {
      name: guid(resourceGroup().id, RoleDefinitionId, id)
      properties: {
        roleDefinitionId: roleDefinition.id
        principalId: id
        principalType: principalType
      }
    }]
    

    Regarding your second question, it sounds quite complex. You probably would need to have a preparation task that will iterate and check which role assignments already exist and then create a complex object to pass to the bicep file. Because the ARM API is idempotent, not sure why are you trying only to deploy the new changes ?