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.
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.
"#################################################"
"# 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.
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:
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.