Speed up deployments by not running your deploymentScripts every time
In one of our deployments we use deploymentScripts. We use it to apply SQL migration scripts and assign SQL principals and roles in a piece of test software that persists metrics for specific test runs.
What I noticed recently is that the deployments are taking 7+ minutes to finish, even when nothing has changed in the infrastructure.
The problem
After reading the docs for a bit, I discovered we set the forceUpdateTag to the current deployment time via a parameter (param deploymentTimestamp string = utcNow('yyyyMMdd-HHmmss')). This property states the following:
Gets or sets how the deployment script should be forced to execute even if the script resource has not changed. Can be current time stamp or a GUID.
Because of the way we set the property, the deployment scripts are invoked EVERY time we run a new deployment. While that might not sound like it should have much impact because it’s just a small PowerShell script, it’s important to understand what a deployment script is doing under the hood.
A deploymentScripts resource does not run inline. It provisions a real Azure Container Instance (ACI) as the execution environment. The full lifecycle per script execution is:
- ARM provisions an ACI container and storage account
- The ACI pulls the specified container image, in our case
azPowerShellVersion: '14.0', a multi-hundred-MB image - The PowerShell script executes inside the container
- On success (
cleanupPreference: 'OnSuccess'), the ACI is torn down along with the storage account
This lifecycle can take 3-5 minutes per script under normal conditions, not because the SQL work takes a long time, but because of the ACI cold-start overhead. Because we have two deployment scripts with a dependency between them, they run sequentially and each deploys its own infrastructure, doubling the total time.
The solution
To solve this issue, we need to provide the forceUpdateTag with a value that only changes when the migration script or SQL principals and roles change.
The first one is easy: we create a unique string from the Base64-encoded migration script.
The second one is pretty similar: we serialize the SQL role assignments and create a unique string from those as well.
This should be good enough for our use case.
evaluationSqlSchemaBase64 = base64(loadTextContent(...))
forceUpdateTagMigrationScript: uniqueString(evaluationSqlSchemaBase64)
var sqlPrincipalRoleAssignments = concat(
[
{
principalName: 'mi-test-workload-${take(replace(evaluationIdentity.outputs.principalId, '-', ''), 8)}'
objectId: evaluationIdentity.outputs.principalId
principalType: 'ServicePrincipal'
//...
}
]
forceUpdateTagRolePrincipals: uniqueString(string(sqlPrincipalRoleAssignments))
resource sqlPrincipalRolesDeploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: name
location: location
kind: 'AzurePowerShell'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${deploymentScriptIdentityResourceId}': {}
}
}
properties: {
azPowerShellVersion: '14.0'
timeout: timeout
cleanupPreference: 'OnSuccess'
retentionInterval: 'P1D'
forceUpdateTag: forceUpdateTagMigrationScript // or forceUpdateTagRolePrincipals, depending on the resource.
scriptContent: '''
$ErrorActionPreference = 'Stop'
# ...
'''
environmentVariables: [
// ...
]
}
}
With this update, the deployment time is reduced from 7-10 minutes to 2-3 minutes, which is a significant improvement when you’re waiting for the test results to show up.
