Deploying Bicep from Azure DevOps with the new BicepDeploy@0 task

No more Azure CLI, Azure PowerShell, or ARM task

Deploying Bicep from Azure DevOps with the new BicepDeploy@0 task

For a long time, deploying Bicep through Azure DevOps wasn't a clear choice. You could use the Azure resource group deployment task. You could have created something custom with the Azure CLI. Or you used a combination with the Azure PowerShell task and used the available PowerShell commands.

All of those solutions work. But they don't quite feel like a native Bicep experience. That finally changed.

A few months ago, the Bicep team already introduced a GitHub Actions experience for Bicep deployment. After that was released, you saw it already coming that another request for Azure DevOps landed.

Now, if you looked at the recent Bicep community call, I was happy to see Sneha Bandla present the new native task BicepDeploy@0. The task can work directly with .bicep or .bicepparam files, install and cache the Bicep CLI version, and run validation operations (including what-if).

In this post, you will see where BicepDeploy@0 fits, what problem it solves, and how to start using it in an Azure DevOps pipeline.

A quick look at Bicep Deploy

Before looking at the new task, it's good to look back a bit. The Bicep CLI has always been laid-back when it comes to deployment automation. Yes, the language is a first-class citizen to ARM, but the deployment command came from somewhere else. As mentioned in the introduction, you used the Azure CLI with commands such as az deployment group create or az deployment sub create. The same would be applicable for Azure PowerShell (New-AzResourceGroupDeployment or New-AzSubscriptionDeployment.

Those commands deploy Bicep just fine, but the pipeline still requires some customization inside scripts. Think about which scope your Bicep file requires to land, how parameters are passed in, or how you invoke what-if if you needed it.

Then, the groundwork formed. In v0.38.3, the bicep deploy command was introduced. And that work led to a better deploy experience natively from the CLI itself. Instead of compiling .bicep files to ARM, the pipeline can describe a Bicep deployment directly. That would mean:

  • Here you have the template
  • These are the parameters
  • This is the scope
  • And this is the Bicep version I want

For DevOps pipelines, this matters a lot because local commands are usually easy to implement and fix.

Why use BicepDeploy@0?

You can already guess why you should use BicepDeploy@0: it just removes the glue code you've been (probably) doing for years.

Take, for example, a typical Azure DevOps pipeline where you use the Azure CLI task:

- task: AzureCLI@2
  displayName: Deploy Bicep template
  inputs:
    azureSubscription: <azureSubscription>
    scriptType: bash
    scriptLocation: inlineScript
    inlineScript: |
      az deployment group create \
        --resource-group my-resource-group \
        --template-file infra/main.bicep \
        --parameters infra/main.bicepparam

Or for Azure PowerShell:

- task: AzurePowerShell@5
  displayName: Deploy Bicep template
  inputs:
    azureSubscription: <azureSubscription>
    ScriptType: InlineScript
    Inline: |
      New-AzResourceGroupDeployment `
        -ResourceGroupName 'my-resource-group' `
        -TemplateFile 'infra/main.bicep' `
        -TemplateParameterFile 'infra/main.bicepparam'
    azurePowerShellVersion: LatestVersion

Everyone deploying Bicep files should be familiar with the above examples. But they push deployment behavior into scripts. Now, the scope above is for a resource group. The pipeline author has to know the right command for every scope. And that should further translate into pipeline inputs.

With the new BicepDeploy@0 task, the same deployment becomes a task configuration:

- task: BicepDeploy@0
  displayName: Deploy Bicep template
  inputs:
    azureResourceManagerConnection: <azureSubscription>
    subscriptionId: $(subscriptionId)
    resourceGroupName: my-resource-group
    templateFile: infra/main.bicep
    parametersFile: infra/main.bicepparam
💡
By default, the scope is resourceGroup, thus omitted from the above code snippet. Other scopes, such as subscriptions, tenants, or management groups, are also supported.

With the task defined, it isn't hidden in a shell script. It just says what it is going to do through Bicep-specific task inputs.

Pin the Bicep version

I hadn't even shown the Bicep version in the previous section, but that was explicitly left out because that's something you've to do as well. And many folks complained about it.

The Bicep CLI isn't installed on all runners, let alone when you host your own. Most of the time, you would just install the latest Bicep CLI version on your agent, and sometimes, this would break.

One of the more practical inputs to solve it easily now is to specify the bicepVersion in the BicepDeploy@0 task:

- task: BicepDeploy@0
  displayName: Deploy with pinned Bicep version
  inputs:
    azureResourceManagerConnection: <azureSubscription>
    subscriptionId: $(subscriptionId)
    resourceGroupName: my-resource-group
    templateFile: infra/main.bicep
    parametersFile: infra/main.bicepparam
    bicepVersion: 0.38.5

This automatically solves the installation step. I also prefer to pin versions deliberately, especially in important pipelines I'm running.

Validate and preview before you deploy

The other benefit of using this task is the fact that you can run two different operations to validate and preview by using validate or whatIf.

To insert a validation step early in the pipeline, you could use something like this:

- task: BicepDeploy@0
  displayName: Validate Bicep template
  inputs:
    operation: validate
    azureResourceManagerConnection: <azureSubscription>
    subscriptionId: $(subscriptionId)
    resourceGroupName: my-resource-group
    templateFile: infra/main.bicep
    parametersFile: infra/main.bicepparam
    validationLevel: providerNoRbac

If the template is correct, you can do a what-if step afterward before actually applying the infrastructure:

- task: BicepDeploy@0
  displayName: Preview Bicep changes
  inputs:
    operation: whatIf
    azureResourceManagerConnection: <azureSubscription>
    subscriptionId: $(subscriptionId)
    resourceGroupName: my-resource-group
    templateFile: infra/main.bicep
    parametersFile: infra/main.bicepparam
    whatIfExcludeChangeTypes: noChange

You can choose the validation level for deployment validate and what-if operations. The task supports provider, template, and providerNoRbac, which gives you a way to tune how deep validation should go for the environment and permissions available to that pipeline.

Using deployment stacks

The other big reason to pay attention to this task is the support for deployment stacks.

In short, deployment stacks let Azure track the resources managed by a deployment and define what should happen when resources are no longer present in the template.

💡
For more information on deployment stacks, check out the article on Microsoft Learn.

With BicepDeploy@0, you can set type to deploymentStack and configure those options:

- task: BicepDeploy@0
  displayName: Deploy production stack
  inputs:
    type: deploymentStack
    operation: create
    name: production-stack
    azureResourceManagerConnection: <azureSubscription>
    subscriptionId: $(subscriptionId)
    resourceGroupName: production-rg
    templateFile: infra/main.bicep
    parametersFile: infra/production.bicepparam
    actionOnUnmanageResources: delete
    denySettingsMode: denyWriteAndDelete

If you want to go for a safer option first, start with actionOnUnmanageResource: detach. This just keeps the resources alive in Azure, but it isn't managed by the stack anymore.

There are additional stack-specific inputs. However, these are perfectly described in the article on Microsoft Learn. The important point is that those are task inputs now, rather than creating something custom (again).

Capture outputs for later steps

Capturing outputs is another place where BicepDeploy@0 shines compared to the older script-based approach. Take Azure PowerShell. Typically, you caputre the deployment result and walk through the Outputs object yourself:

- task: AzurePowerShell@5
  name: deploy
  displayName: Deploy infrastructure
  inputs:
    azureSubscription: <azureSubscription>
    ScriptType: InlineScript
    Inline: |
      $deployment = New-AzResourceGroupDeployment `
        -ResourceGroupName 'my-resource-group' `
        -TemplateFile 'infra/main.bicep'

      foreach ($output in $deployment.Outputs.GetEnumerator()) {
        Write-Host "##vso[task.setvariable variable=$($output.Key);isOutput=true]$($output.Value.Value)"
      }
    azurePowerShellVersion: LatestVersion

With the BicepDeploy@0, Bicep's output just becomes part of the pipeline variables for later usage. You just have to give the step a name and you can reference it later:

- task: BicepDeploy@0
  name: deploy
  displayName: Deploy infrastructure
  inputs:
    azureResourceManagerConnection: <azureSubscription>
    subscriptionId: $(subscriptionId)
    resourceGroupName: my-resource-group
    templateFile: infra/main.bicep

- task: PowerShell@2
  displayName: Use deployment outputs
  inputs:
    targetType: inline
    script: |
      Write-Host "Storage Account Name: $(deploy.storageAccountName)"
      Write-Host "Web App URL: $(deploy.webAppUrl)"

If your template emits sensitive values, use maskedOutputs to list outputs that should be masked in logs.

When to replace existing tasks

Don't worry. You don't need to rewrite every pipeline immediately. If a pipeline has a lot of custom logic around deployment, you're going to need to translate that context.

But for straightforward Bicep deployments you've seen in this article, BicepDeploy@0 is a better default than a generic ARM deployment task (or az and New-Az command).

I would start with new pipelines first. Use the native task, templatize it so you can have different inputs for different use cases, and choose deployment stacks when you're ready for it.

Then look at existing pipelines that mostly wrap az deployment or New-*AzDeployment code. Those are going to be good candidates for migration.

Conclusion

This new BicepDeploy@0 task for Azure DevOps helps you to define a more direct way to describe Bicep deployment, just as it did for GitHub Actions.

You can point it at .bicep and .bicepparam files, pick your operation with scope, and don't forget to pin the Bicep CLI version. That's what I like about this new task. Fewer parts to move in every pipeline, including clearer intent in YAML. And finally, Bicep became a first-class citizen for deployments.

For the full documentation on the task, head to this link.