Run PowerShell scripts with Azure Bicep

Did you know you can run Azure PowerShell or CLI scripts with Bicep? With Microsoft.Resources/deploymentScripts you can execute scripts in a Bicep deployment. In addition, you can work with the outputs of the script that you ran. This opens loads of automation possibilities and flexibility.

What are Deployment Scripts?

The deploymentScript resource can run PowerShell or Bash scripts that are run inside a temporary container. The given scripts run during an ARM deployment, so besides infrastructure deployments, it is possible to call internal or external APIs or gather resource information before deploying infrastructure. 

DeploymentScript uses User-Assigned Managed Identities, or Connect-AzAccount/az login. This identity connects to Azure and runs the scripts inside the container. A managed identity can get an RBAC (or Azure AD) role, this helps to scope what a deployment scripts can control, deploy or delete.

Once the deployment script is finished, it is possible to get the outputs of a script. These outputs can be used to populate properties in other resource deployments.

How does it work?

When the resource type Microsoft.Resources/deploymentScripts is deployed the Azure Resource Manager deploys the following resources in the given resource group:

  • Azure Container Instance – is used for running scripts.
  • Azure Storage Account – is used to store script outputs in a file share.
  • Deployment Script – is used for debugging or logging.

The Container Instance and the Storage Account are temporary resources. The ARM engine will automatically clean up these resources once the deployment script has been completed. Billing is minimal when you let the ARM engine clean up the resources. It is also possible to use existing container instances and storage accounts however, these do not clean up automatically and have to be managed by yourself.

How does this work in practice?

To demonstrate the working of deployment scripts, we want to deploy the following resources:

  • User-Assigned Managed Identity
  • Deployment Script (and indirectly also a container instance and storage account)
  • Azure Key Vault

The goal of this Bicep deployment is to create an Azure Key Vault, along with a service principal that is granted access to the policy of the key vault.

param parLocation string = 'westeurope'
var varTenantId = tenant().tenantId
resource resManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = {
name: 'john-managed-identity'
location: parLocation
}
resource resDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
name: 'create-spn-for-kv'
location: parLocation
kind: 'AzurePowerShell'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${resManagedIdentity.id}' : {}
}
}
properties: {
azPowerShellVersion: '9.0'
retentionInterval: 'P1D'
scriptContent: '''
$spnAppId = New-AzADServicePrincipal -DisplayName "my-keyvault-spn" | Select-Object -ExpandProperty AppId
$DeploymentScriptOutputs = @{}
$DeploymentScriptOutputs['appId'] = $spnAppId
'''
}
}
resource resKeyVault 'Microsoft.KeyVault/vaults@2019-09-01' = {
name: 'my-ds-key-vault'
location: parLocation
properties: {
enabledForDeployment: true
enabledForTemplateDeployment: true
enabledForDiskEncryption: true
tenantId: varTenantId
accessPolicies: [
{
tenantId: varTenantId
objectId: resDeploymentScript.properties.outputs.appId
permissions: {
keys: [
'get'
]
secrets: [
'list'
'get'
]
}
}
]
sku: {
name: 'standard'
family: 'A'
}
}
}

Deployment Scripts resource

resource resDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
name: 'create-spn-for-kv'
location: parLocation
kind: 'AzurePowerShell'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${resManagedIdentity.id}' : {}
}
}
properties: {
azPowerShellVersion: '9.0'
retentionInterval: 'P1D'
scriptContent: '''
$spnAppId = New-AzADServicePrincipal -DisplayName "my-keyvault-spn" | Select-Object -ExpandProperty AppId
$DeploymentScriptOutputs = @{}
$DeploymentScriptOutputs['appId'] = $spnAppId
'''
}
}

Let’s take a look at the deployment script resource Microsoft.Resources/deploymentScripts. In this Bicep resource several properties are important:

  • kind

This specifies the type of the script. Only Azure PowerShell or Azure CLI scripts are supported, but not limited to Azure PowerShell or Azure CLI commands. Values are: AzurePowerShell or AzureCLI.

  • identity

Not required, but an identity is needed in some way. The identity property uses a user-assigned managed identity that is created in an earlier stage in the Bicep deployment resManagedIdentity.id. Currently, only user-assigned managed identities can be used. If no identity is given, the deployment script has to call Connect-AzAccount in PowerShell or az login in AzureCLI.

  • azPowerShellVersion / azCliVersion

Azure PowerShell or Azure CLI version to be used when running script. Not all versions are supported, but Microsoft shared a list of supported versions for Azure PowerShell here or Azure CLI here.

  • retentionInterval

The retention interval is used to determine how long the script results should be stored. By default, the results are erased after running the script. In the example, I used P1D, which stands for “one day”. The script refers to the deployment script resource.

  • scriptContent

The script to run. This can be a multi-line ''' inline script or the loadTextContent() Bicep function can be used load a script from a file.

The output of the example

If we run the demonstration example a few resources are being created:

The resources defined in the example are key vault, managed identity and deployment scripts. The storage account and container instances are created by the deployment script and the names are randomly generated.

In the deployment script resource we can see that the container instance and storage account are linked and that they are removed after the provision state is succeeded.

The script created a service principal and used the appId as an output. This output can be found in the outputs of the deployment script.

The deployment script output is used in the access policies of the key vault. The created service principal has permission to the created key vault. 

Deployment Script outputs

In the script contents, outputs can be defined that can be utilized elsewhere in the Bicep template. The definition of deployment script outputs is different between Azure PowerShell and Azure CLI:

Azure PowerShell

$DeploymentScriptOutputs = @{}
$DeploymentScriptOutputs['appId'] = $spnAppId

For Azure PowerShell, you have to define a hashtable with the name DeploymentScriptOutputs

Azure CLI

$spnAppId > $AZ_SCRIPTS_OUTPUT_PATH

For Azure CLI, you have to output the value to the $AZ_SCRIPTS_OUTPUT_PATH variable.

Use the output in Bicep

resource resDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
...
}
output outScriptOutput string = resDeploymentScript.properties.outputs.appId

To use the outputs generated by the Azure PowerShell or CLI script refer to the symbolic name of the resource. This resource contains the property .properties.outputs. which has the value of $spnAppId stored in appId.

Deployment Script input parameters

As you can see, we are creating a service principal with a predefined name. We can make this name adjustable by populating it with parameters supplied by Bicep.

param parSpnName string = 'my-input-parameter'
### hidden managed identity resource ###
resource resDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
name: 'create-spn-for-kv'
location: parLocation
kind: 'AzurePowerShell'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${resManagedIdentity.id}' : {}
}
}
properties: {
azPowerShellVersion: '9.0'
retentionInterval: 'P1D'
arguments: '-SpnName ${parSpnName}'
scriptContent: '''
param (
[string] $SpnName
)
$spnAppId = New-AzADServicePrincipal -DisplayName $SpnName | Select-Object -ExpandProperty AppId
$DeploymentScriptOutputs = @{}
$DeploymentScriptOutputs['appId'] = $spnAppId
'''
}
}

In the above Bicep we added the parameter parSpnName which is used as input for the PowerShell script. Also, the script is modified to support parameters. We added the parameter block that contains $SpnName to supply the script with the given service principal name. To pass through the value of parSpnName the arguments property was added. This property corresponds with the parameter required by the PowerShell script.

If we check the Azure Portal we see our deployment script resource create-spn-for-kv. Under content and inputs we see the property arguments. We can see that the dynamic value (${parSpnName}) has been changed to the given parameter value. In this case the hardcoded value my-input-parameter.

Private networking

With the release of the new deployment scripts resource API version Microsoft.Resources/deploymentScripts@2023-08-01 it is now possible to run deployment scripts in a private network environment.

This means that the Azure Container Instance created by the deployment script is running privately in a virtual network, and it can connect to an existing storage account over a private endpoint.

In the architecture overview provided below, we can see that the deployment script executes a script within a container instance deployed within a delegated subnet. Through this subnet, the container instance can communicate with the private endpoint configured storage account. This private endpoint is registered within a private DNS zone, which is associated with the virtual network.

In addition to the network setup, there is also a user-assigned managed identity configured with precise permissions for the storage account.

Architecture overview of private deployment scripts

As seen in the architecture overview, several resources are required to showcase private deployment scripts:

  • Virtual network with 2 subnets
    • Subnet for private endpoint
    • Subnet for the Azure Container Instance, which has delegation configured to Microsoft.ContainerInstance/containerGroups
  • Storage account
    • Configured with a private endpoint to target sub-resource file
  • User Assigned Managed Identity
    • Configured with Storage File Data Privileged Contributor permissions on the storage account
  • Deployment script
    • Initiates the creation of the Azure Container Instance
  • Private DNS zone

The Azure Bicep configuration required to deploy the Azure Container Instance in a subnet is very minimal. The 2023-08-01 version introduces the property subnetIds under containerSettings which handles the container instance deployment in the subnet.

If we run the Azure Bicep below the deployment script will deploy the container instance within the given subnet and runs the script content privately:

resource resPrivateDeploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: 'my-private-deployment-script'
location: parLocation
kind: 'AzurePowerShell'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${resManagedIdentity.id}' : {}
}
}
properties: {
storageAccountSettings: {
storageAccountName: resStorageAccount.name
}
containerSettings: {
subnetIds: [
{
id: resVirtualNetworkRef::resContainerInstanceSubnetRef.id
}
]
}
azPowerShellVersion: '9.0'
retentionInterval: 'P1D'
scriptContent: 'Write-Host "Hello World!"'
}
}

To maintain simplicity, I concentrated solely on the deployment script resource. If you are interested in the full deployment, check out the full Bicep template in my bicep-snippets repository: privateDeploymentScripts.bicep

Conclusion

This is how you can use deployment scripts in your Bicep templates. It offers new possibilities and flexibility regarding infrastructure automation. In this blog post we talked about creating a service principal, but it can also be used to look up IP-address data, for creating certificates and saving it in a key vault and so on. 

If you want to have a deployment script Bicep snippet check out my GitHub repository: bicep-snippets.

UPDATE 10-10-2023:

Updated blog post because of the release of private deployment scripts. Added “Private networking” to explain the working of private deployment scripts.

2 thoughts on “Run PowerShell scripts with Azure Bicep

  1. Thank you for the private networking section, this is super clear and useful. I ran into a situation after the container instance is being created, it sits in “Waiting” state forever. I think you mentioned in github that in your case, you forgot the private dns zone for “file”. I do have that, and the private endpoint does resolve correctly. Do you have any suggestions on how to further troubleshoot this? I am using the latest bicep 2023-08-01 as you stated above.

    Like

Leave a comment