Running scripts inside of your Bicep files

Jul 1, 2022 5:46 AM

Personal Blog
Microsoft
Azure
IaC
Bicep
Azure CLI
Powershell

Getting started

In one of my previous blogs, I showed how to use the existing attribute to get certain details such as skus from already existing resources. But as mentioned, this can't be made conditional and if the check with existing fails, your whole deployment fails.

This can only be accomplished with custom Powershell/Bash scripts inside of your Bicep files. In this blog we will look at how to use these scripts in Bicep and we will use the example of how to check a resource exists, then output this to a variable and use this variable to run or skip the modules with the existing check.

Scripting in Bicep

Bicep allows for two types of scripting languages, you can use either PowerShell or Azure CLI (which allows Bash). To use scripts within bicep we need to look at the Microsoft.Resources/deploymentScripts@2020-10-01 resource. Deployment Scripts allows us to use custom powershell/bash scripts in our deployments and do the little things that bicep itself can't do. A good example would also be add your MSI to the Microsoft Graph as shown in another previous blog.

The basic Bicep for Powershell would look something like this:

resource symbolicname 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
  name: ''
  location: ''
  kind: 'AzurePowerShell'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
}
  }
  properties: {
    forceUpdateTag: ''
    azPowerShellVersion: ''
    timeout: ''
    arguments: ''
    scriptContent: ''
    cleanupPreference: ''
    retentionInterval: ''
  }
}

And for Azure CLI, it would look something like this:

resource symbolicname  'Microsoft.Resources/deploymentScripts@2020-10-01' = {
  name: ''
  location: ''
  kind: 'AzureCLI'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {

}
  }
  properties: {
    forceUpdateTag: ''
    azCliVersion: ''
    timeout: ''
    arguments: ''
    scriptContent: ''
    cleanupPreference: ''
    retentionInterval: ''

  }
}

Both are very similar in how they are structured, only the value for the Kind property is different, as well as within properties, where they each got their own property for their version (azPowerShellVersion & azCliVersion). For more information about which properties are available, you can check the docs.

Now that we know how to structure our Bicep for a custom script, let's have a look at the example to check if a resource exists!

Check resources

To check this, we will need our Bicep file, our Script and to generate an output to use within our other Bicep files. When bringing this all together something like this will be the outcome:

Azure CLI

param resourceName string
param location string
param resourceGroupName string = resourceGroup().name
param utcValue string = utcNow()
param UserAssignedIdentity string

resource resource_exists 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
  name: 'exist_${resourceName}'
  location: location
  kind: 'AzureCLI'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${UserAssignedIdentity}': {}
    }
  }
  properties: {
    forceUpdateTag: utcValue
    azCliVersion: '2.37.0'
    timeout: 'PT10M'
    arguments: '\'${resourceGroupName}\' \'${resourceName}\''
    scriptContent: 'result=$(az resource list --resource-group ${resourceGroupName} --name ${resourceName}); echo $result | jq -c \'{Result: map({name: .name})}\' > $AZ_SCRIPTS_OUTPUT_PATH'
    cleanupPreference: 'OnSuccess'
    retentionInterval: 'P1D'
  }
}

output exists bool = length(resource_exists.properties.outputs.Result) > 0

Powershell

param resourceName string
param location string
param resourceGroupName string = resourceGroup().name
param utcValue string = utcNow()
param UserAssignedIdentity string

resource resource_exists 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
  name: 'exist_${resourceName}'
  location: location
  kind: 'AzurePowerShell'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${UserAssignedIdentity}': {}
    }
  }
  properties: {
    forceUpdateTag: utcValue
    azPowerShellVersion: '8.0'
    timeout: 'PT10M'
    arguments: '-resourceName ${resourceName} -resourceGroupName ${resourceGroupName}'
    scriptContent: '''
     param(
       [string] $resourceName, 
       [string] $resourceGroupName
       )
     $Resource = Get-AzResource -Name $resourceName -ResourceGroupName $resourceGroupName
     $ResourceExists = $null -ne $Resource
     $DeploymentScriptOutputs = @{}
     $DeploymentScriptOutputs['Result'] = $ResourceExists
     '''
    cleanupPreference: 'OnSuccess'
    retentionInterval: 'P1D'
  }
}

output exists bool = resource_exists.properties.outputs.Result

Both the az resource list and Get-AzResource command will list the resource within the specified resourceGroup with the given resourceName. If the resource exists, it will return its name, meaning it will return back more than 0 characters of a string value. This we can then check with the length function in our output for the Azure CLI, where we do a simple comparison for Powershell. If anything exists it will return a 1 (true) or a 0 (false). With this we can continue to conditionalize our deployment.

NOTE: It is important to use the UserAssignedIdentity, otherwise the scripts won't work. The docs state it is optional, but without it the script will fail. Do make sure that the UserAssignedIdentity used has the proper permissions, such as the Contributor role.

In our main.bicep file we can now make the module containing a separate existing check conditional, using our Output. This might look something like this:

module sqlDbSKU 'Modules/sqlDbSKU.bicep' = if (existcheck.outputs.exists) { 
  name: 'DBSKU'
  params: { 
    DBName: DBName
  } 
}

What's next?

Entering July, the school holidays will begin here in the Netherlands and some colleagues will be off, which means there is more work to be done. Stay tuned for next weeks blog!