Bicep | Deployment Scripts

Intro and use cases

For a project I needed to create AAD groups while only having a single Bicep/ARM deployment (including UI Wizard)… I decided to go with deployment scripts (Powershell) and a user-assigned Managed Identity. This approach can also be used for Azure Market Place offers.

Deployment scripts in ARM/Bicep enable custom automation for your environment management. You use deployment scripts to execute your own scripts within your ARM template deployments.

Possible use cases (not complete):

  • create resources which can’t be described using Bicep, e.g.:
    • AAD users / groups
    • External identities settings like catalogs and access packages
    • Identity governance settings like connected organizations
  • consume external APIs

Outputs from deployment scripts can be “returned” (e.g created AAD group identifiers).

Scenario for this blog post

I created a small project to outline different options in Bicep:

  • describe user “john.contoso@hoferlabs.ch“ (deployment script)
  • describe group “Sales” and describe Johns Membership (deployment script)
  • describe storage account and a file share (Bicep)
  • describe group Azure roles Storage File Data SMB Share Contributor (Bicep)

In this post, we focus on the deployment scripts and the required setup. You can find the newest versions in my public Repo.

Solution

Setup Script

Deployment scripts don’t use the context (/permissions) of the user which deploys the Bicep template, instead they can use a user-assigned Managed Identity which needs the required permissions. And because deployments scripts will automatically create a Storage Account and Container, we also need a temporary deployment resource group.

setup.ps1setup.ps1
"#################################################" "# hoferlabs.ch #" "# implement requirements for deployment script #" "# Version: 0.1 #" "#################################################" # edit here $strDeploymentResourceGroupName = "rg-deploymentScript-10" $strUserAssignedManagedIdentityName = "mi-deploymentScript-10" $strLocation = "Switzerlandnorth" $arrPermissions = "User.ReadWrite.All", "Group.ReadWrite.All", "Directory.ReadWrite.All", "GroupMember.ReadWrite.All", "RoleManagement.ReadWrite.Directory" # "RoleManagement.ReadWrite.Directory" is needed if we create a group which can be assigned Azure roles # do not edit $strGraphAppId = "00000003-0000-0000-c000-000000000000" Connect-MgGraph -Scopes Application.Read.All, AppRoleAssignment.ReadWrite.All, RoleManagement.ReadWrite.Directory $objContext = Get-AzContext "connected to subscription $($objContext.Subscription.Name) ($($objContext.Subscription.Id)) with account $($objContext.Account)" "create deployment resource group $strDeploymentResourceGroupName..." $objResourceGroup = New-AzResourceGroup -Name $strDeploymentResourceGroupName -Location $strLocation "create user-assigned managed identity $strUserAssignedManagedIdentityName..." $objUserAssignedManagedIdentity = New-AzUserAssignedIdentity -ResourceGroupName $strDeploymentResourceGroupName -Name $strUserAssignedManagedIdentityName -Location $strLocation "waiting for user-assigned managed identity to be created..." start-sleep 50 "get graph app..." $objGraphApp = Get-MgServicePrincipal -Filter "AppId eq '$strGraphAppId'" "assign permissions..." ForEach ($strPermission in $arrPermissions) { " assign permission $strPermission..." $objRole = $objGraphApp.AppRoles | Where-Object { $_.Value -eq $strPermission } $objResult = New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $($objUserAssignedManagedIdentity.PrincipalId) -PrincipalId $($objUserAssignedManagedIdentity.PrincipalId) -ResourceId $objGraphApp.Id -AppRoleId $objRole.Id } "verify permissions..." $arrRoleAssignment = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $($objUserAssignedManagedIdentity.PrincipalId) ForEach ($objRoleAssignment in $arrRoleAssignment) { $objRole = $objGraphApp.AppRoles | Where-Object { $_.Id -eq $objRoleAssignment.AppRoleId } " assignment for app role $($objRole.Value) was created on $($objRoleAssignment.CreatedDateTime) (ID: $($objRoleAssignment.AppRoleId))" } "remove the resource group (and all its resources) with the following command after the deployment: Remove-AzResourceGroup $strDeploymentResourceGroupName -Force"

Output of the deployment script:

A dedicated blog post about assigning permissions to user-assgined managed identities is available here.

Bicep Template

This is the main template. You can find the newest versions and all submodules in my public Repo.

mainTemplate.bicepmainTemplate.bicep
param uamiName string = 'mi-deploymentScript-10' param userPrincipalName string = 'john.contoso@hoferlabs.ch' param userDisplayName string = 'John Contoso' param groupName string = 'Sales' param groupDescription string = 'Sales Group' param storageAccountName string = 'sahoferlabssales' param storageAccountType string = 'Standard_LRS' param storageAccountFileShareName string = 'fshoferlabssales' param targetResourceGroup string = 'rg-target-10' targetScope = 'resourceGroup' // describe deployment script for the AAD user module deploymentScriptAadUser 'modules/createAadUser.bicep' = { name: 'aadUserDeployment' dependsOn: [] params: { uamiName: uamiName userPrincipalName: userPrincipalName userDisplayName: userDisplayName } } // describe deployment script for the AAD group module deploymentScriptAadGroup 'modules/createAadGroup.bicep' = { name: 'aadGroupDeployment' dependsOn: [ deploymentScriptAadUser ] params: { uamiName: uamiName groupName: groupName groupDescription: groupDescription } } // describe deployment script for the AAD group membership module deploymentScriptAadGroupMembership 'modules/assignAadGroupMembershipToUser.bicep' = { name: 'aadGroupMembershipDeployment' dependsOn: [ deploymentScriptAadGroup ] params: { uamiName: uamiName userId: deploymentScriptAadUser.outputs.userId groupId: deploymentScriptAadGroup.outputs.groupId } } // describe storage account module storageAccount 'modules/storageAccount.bicep' = { name: 'storageAccountDeployment' dependsOn: [] scope: resourceGroup(targetResourceGroup) params: { storageAccountName: storageAccountName storageAccountType: storageAccountType } } // describe file share module storageAccountFileShare 'modules/storageAccountFileShare.bicep' = { name: 'storageAccountFileShareDeployment' dependsOn: [ storageAccount ] scope: resourceGroup(targetResourceGroup) params: { storageAccountName: storageAccountName storageAccountFileShareName: storageAccountFileShareName } } // describe storage account permissions module storageAccountAzureRoleAssignment 'modules/storageAccountRoleAssignment.bicep' = { name: 'storageAccountAzureRoleAssignmentDeployment' dependsOn: [ deploymentScriptAadGroup, storageAccountFileShare ] scope: resourceGroup(targetResourceGroup) params: { principalId: deploymentScriptAadGroup.outputs.groupId roleDefinitionId: '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb' // Storage File Data SMB Share Contributor storageAccountName: storageAccountName } }

Deployment and clean up

We can either build the ARM File using az bicep build --file .\mainTemplate.bicep or deploy the ARM directly:
Deploy to Azure

The UI looks terrible, we will deal with that later.

After a while, the deployment succeeds:

To delete the deployment resource group, use the command from the setup script.

Details / Remarks

Parameters

An important part is that we are able to pass parameters to a deployment script and from a deployment script back to Bicep.

// use the current time in the name, otherwise the script isn't triggered again 
resource createAadUser 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
  name: 'createAadUser-${utcValue}'
  location: location
  kind: 'AzurePowerShell'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${userAssignedManagedIdentity.id}': {}
    }
  }
  properties: {
    azPowerShellVersion: '8.3'
    forceUpdateTag: utcValue
    scriptContent: loadTextContent('createAadUser.ps1')
    arguments: '-userPrincipalName \\"${userPrincipalName}\\" -userDisplayName \\"${userDisplayName}\\"'
    retentionInterval: 'P1D'
    cleanupPreference: 'OnExpiration'
  }
}

output userId string = createAadUser.properties.outputs.userId
output result string = createAadUser.properties.outputs.result

We require AZ Powershell version 8.3 and define the user-assigned managed identity. We pass two arguments to the deployment script. We use \\" to escape the ", that way we are able to pass parameters with spaces (e.g. group description).

At the bottom, we see the expected output from the deployment script. In Powershell we have to create a hashtable:

$DeploymentScriptOutputs = @{}
$DeploymentScriptOutputs["userId"] = $($objUser.id)
$DeploymentScriptOutputs["result"] = $result

In Bicep, we can pass the Module outputs to other modules:

module deploymentScriptAadGroupMembership 'modules/assignAadGroupMembershipToUser.bicep' = {
  name: 'aadGroupMembershipDeployment'
  dependsOn: [ deploymentScriptAadGroup ]
  params: {
    uamiName: uamiName
    userId: deploymentScriptAadUser.outputs.userId
    groupId: deploymentScriptAadGroup.outputs.groupId
  }
}

Redeployments

Because our deployments scripts are designed for multiple deployments (IaC approach) we can rerun the deployment with the same parameters again and it still succeeds.

It’s important that we only create resources (like AAD users) if they don’t already exist and also output needed parameters (like the user id).

Troubleshoot deployment scripts

We can connect to the Container instance which is running our deployment script:

To run Powershell commands, we have to start Powershell first using the pwsh command:

As you can see, the Container runs as our defined user-assigned managed identity. We can run and test our deployment scripts directly in the container instance.

You can set the retention of the deployment resources in Bicep (retention of storage account/container for the deployment).

UI Wizard

We will replace the ugly default UI page with a nicer version in the next post.