Keep Pumping Bicep: Integrate AZ CLI Seamlessly
Juan Manuel Servera Bondroit
Sr. Cloud Solution Adventurer - AppDev at Microsoft
When you are experimenting in the Azure portal
However, life isn’t always that simple. Sometimes, Bicep alone can't handle complex operations. Take domain validation in a Static Web App, for instance. This task involves starting a blocking validation operation, grabbing a token, creating a TXT record in your DNS
That’s where Bicep Deployment Scripts come to the rescue. This handy feature lets you run AZ CLI or PS commands directly within your Bicep workflow, allowing you to extract values and seamlessly integrate them into your template. It's like having your cake and eating it too—smooth, efficient, and all in one place.
1. Creating the Basic Assets
To kick things off, we'll set up a Managed Identity with the least privilege required to run our tasks. This identity will be used to run your script later on.
@description('Location for the creation of the identity, takes the location of the resource group by default')
param location string = resourceGroup().location
param tags object
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'uai-deployment-static-${substring(uniqueString(resourceGroup().id),0, 4)}'
location: location
tags: tags
}
// Assign the identity the "Reader" role on the resource group
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid('acdd72a7-3385-48ef-bd42-f606fba81ae7', identity.name, subscription().subscriptionId, resourceGroup().name)
properties: {
principalId: identity.properties.principalId
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
principalType: 'ServicePrincipal'
}
}
@description('Generated name for the identity')
output identity_name string = identity.name
@description('Generated id for the identity')
output identity_id string = identity.id
We will also need to add a custom role so the identity can start the validation process:
param identityName string
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
name: identityName
}
// Creates the custom role to access the actions needed for the deployment script
// Using least privilege principle
resource customRole 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' = {
name: guid(
'deployment-script-minimum-privilege-for-deployment-principal',
identityName,
subscription().subscriptionId,
resourceGroup().name
)
scope: resourceGroup()
properties: {
roleName: '${resourceGroup().name}-deployment-script-minimum-privilege-for-deployment-principal'
description: 'Configure least privilege for the deployment principal in deployment script'
type: 'customRole'
permissions: [
{
actions: [
'Microsoft.Web/staticSites/customDomains/validate/action'
]
}
]
assignableScopes: [
resourceGroup().id
]
}
}
// assign the custom role to the identity
resource roleAssignmentCustomRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(
'deployment-script-minimum-privilege-for-deployment-principal',
identity.name,
subscription().subscriptionId,
resourceGroup().name
)
properties: {
principalId: identity.properties.principalId
roleDefinitionId: customRole.id
principalType: 'ServicePrincipal'
}
}
2. Creating the Resources
Next, we'll create the Static Web App and add a Custom Domain. This step will initiate the domain verification process, requiring us to run a parallel script to obtain the necessary token.
So, let's imagine you already created a static app, then you will assign a custom domain to it:
@description('Your domain name ex: dev.mydomain.org")
param customDomain string
resource staticSites_domains_start 'Microsoft.Web/staticSites/customDomains@2023-12-01' = {
parent: staticSite
name: customDomain
properties: {
validationMethod: 'dns-txt-token'
}
dependsOn: [
staticSites_dns
]
}
This operation will block until the domain is validated by having a TXT entry in your DNS Zone. As it is blocked, there's no way in Bicep to get the token you need to generate the entry. When you do clickops it's not a big deal, just copy the token you see on screen, open a new tab and create the entry, but when using IaC there's no human involved...
When using a CNAME validation method, you only need to create the CNAME entry for your domain, eliminating the need to run a script. However, this method is not applicable when dealing with subdomains. For instance, I was creating a "dev" subdomain under the main "mydomain.org" with its own DNS Zone. In this scenario, you cannot create an @ entry CNAME in a subdomain DNS Zone... but, this example would be only half the fun, so bear with me as the objective is to learn how to run scripts.
领英推荐
3. Running the Script and Gathering the Token
With the verification process underway, we need to run a script to fetch the token generated by the previous step. Here's where we will run the AZ CLI script to gather it:
param dnsZoneName string
param identityName string
param location string = resourceGroup().location
param staticWebappName string
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
name: identityName
}
// this calls the module where we configure the identity permissions
module deploymentIdentityConfiguration 'deployment-identity-config.bicep' = {
name: 'deployment_identity_configuration'
params: {
identityName: identityName
}
}
resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: 'domain_verification'
location: location
kind: 'AzureCLI'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${identity.id}': {}
}
}
properties: {
azCliVersion: '2.59.0'
retentionInterval: 'PT1H'
arguments: '"${staticWebappName}" "${resourceGroup().name}" "${dnsZoneName}"'
cleanupPreference: 'OnExpiration'
scriptContent: '''
#!/bin/bash
set -e
validationToken=$(az staticwebapp hostname show --name "$1" --resource-group "$2" --hostname "$3" --query validationToken -o tsv)
echo "{'validationToken':'$validationToken'}" > $AZ_SCRIPTS_OUTPUT_PATH
'''
}
dependsOn: [
deploymentIdentityConfiguration
]
}
output validationToken string = deploymentScript.properties.outputs.validationToken
Let's dissect a little bit the script:
4. Filling the Token into the DNS TXT Entry
Finally, we'll complete the domain validation by adding the token into the DNS TXT entry, ensuring our Static Web App is fully verified and ready to roll.
param customDomain string
param validationToken string
resource dnszonesStaticApp 'Microsoft.Network/dnszones@2023-07-01-preview' existing = {
name: customDomain
}
resource dnsZonesStaticAppTXT 'Microsoft.Network/dnsZones/TXT@2023-07-01-preview' = if (validationToken != '') {
parent: dnszonesStaticApp
name: '@'
properties: {
TTL: 1
TXTRecords: [
{
value: [validationToken]
}
]
}
}
Now that you already have the TXT record, the previous process will be able to validate your site. As this is relying on DNS it may take a while until it validates, have a little patience.
Extra examples
And there you have it! This approach has broad applications, like handling other complex domain validations, for example, the Azure Communication Services Email Domain validation:
properties: {
azCliVersion: '2.59.0'
retentionInterval: 'PT1H'
arguments: '"${dnsZoneName}" "${resourceGroup().name}" "${emailServiceName}"'
cleanupPreference: 'OnExpiration'
scriptContent: '''
#!/bin/bash
set -e
az communication email domain initiate-verification --domain-name "$1" --resource-group "$2" --email-service-name "$3" --verification-type Domain
az communication email domain initiate-verification --domain-name "$1" --resource-group "$2" --email-service-name "$3" --verification-type SPF
az communication email domain initiate-verification --domain-name "$1" --resource-group "$2" --email-service-name "$3" --verification-type DKIM
az communication email domain initiate-verification --domain-name "$1" --resource-group "$2" --email-service-name "$3" --verification-type DKIM2
'''
}
Or, as my colleague
Martin Abrle
suggested, running some kubectl commands to prepare your AKS cluster; sometimes working with identities involves running some extra scripts
How does this magic work?
The secret sauce behind this script runner is Azure Container Instances. While there's a minor cost involved in running these scripts, the benefits are substantial. You can leverage advanced features like connecting to a private virtual network or using private endpoints to securely access your resources
Happy stretching and special thanks to AI assistance for contributing to this article.