Keep Pumping Bicep: Integrate AZ CLI Seamlessly

Keep Pumping Bicep: Integrate AZ CLI Seamlessly

When you are experimenting in the Azure portal, it's so tempting to just click around and create resources on the fly. But, as any seasoned Azure pro will tell you, the ClickOps? approach doesn't cut it for production. When it's go-time for your solution, you need a repeatable, reliable method to deploy your infrastructure. This is where Bicep templates cover your Infrastructure as Code (IaC) needs.

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, and waiting for the validation to finish. It’s a multi-step dance that usually requires a script, which often means juggling multiple Bicep templates. Not exactly ideal if you’re looking for a streamlined, single-step deployment.

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:

  • "set -e": this ensures that the script will fail immediatly if something goes wrong with any command. Otherwise the script runner will wait until the script timeout.
  • "retentionInterval: 'PT1H'" and "cleanupPreference: 'OnExpiration'": this indicates the deployment scripts to keep the resources (a Container App and the Storage Account) during some time, in this case 1 hour, and delete them when the hour has passed. This will help on debugging the script, as you will be able to see the logs during some time after the execution.
  • "> $AZ_SCRIPTS_OUTPUT_PATH": when you output a valid JSON inside this variable, the deployment script will parse it and put all the values inside the resource outputs.

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 in the cluster.

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. It’s a small price for a powerful, flexible deployment!

Happy stretching and special thanks to AI assistance for contributing to this article.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了