Azure Deployment Stacks: Zero to Hero 🦾❤️

Photo of author

Dan Rios

9 min read

What is deployment stacks?

Azure Deployment Stacks allow you to manage a set of resources under one roof by defining a Bicep file within stack management. Deployment Stacks build upon and enhance the current deployment methods. Additionally, Deployment Stacks represent the evolution of Azure Blueprints – something many of you may already be familiar with and might still be using today.

Ironically, Deployment Stacks introduce a concept similar to a state file. This means you can now automatically delete resources, much like a Terraform Destroy. However, this state file is managed by Microsoft and abstracted away from the end user. By managing resources via the stack, you can streamline and guardrail your deployments using a much more feature-rich solution than ever before.

In this blog, I’ll dive into the details of Deployment Stacks: what they are, how they can benefit your deployment strategy, and some real-world insights from using stacks.

deployment stacks overview

Azure Blueprints evolution

Azure Blueprints went into public preview over seven years ago; however, it never progressed beyond the preview phase and, as a result, did not reach general availability. Typically, any Azure product that remains in preview for more than 12 months is at significant risk of being retired, so you should expect it to be deprecated.

Deployment Stacks are designed as the successor to Azure Blueprints and, as of mid-2024, have already reached General Availability. This means you can expect ongoing features, improvements, and support from Microsoft. If you are still using Blueprints today, you should start planning your migration to Deployment Stacks as soon as possible.

The focus for the Bicep team moving forward will be heavily centred on Deployment Stacks and their continuous development. Therefore, I strongly recommend transitioning your new and existing deployments into stacks. By doing so, you can enjoy the benefits they offer while also contributing to shaping future features through real-world feedback to the product team.

Where to start?

As always by the teams at Microsoft, there is an abundance of fantastic documentation and learning path modules you can freely leverage to upskill onto the latest and greatest. Deployment stacks is no different, here’s a list of material I would recommend checking out:

Modes

You can change any of the modes in your deployment stack at a later stage and the stack will update the settings. There is also two Deployment Stack built-in RBAC roles that allow granular control of the stack itself.

Action on unmanage

In deployment stacks you can define an “action on unmanage” setting which sets what happens to the resources that are updated or deleted within the stack. There’s three key modes to select in this area and I’ll detail each and how they work.

detachAll

When you delete a resource from your Bicep template in this mode, the resource will no longer be tracked by the deployment stack, but it will still exist in Azure. For instance, if my Bicep template includes a Key Vault module and I delete this module block—or simply give the resource a new name in my Bicep parameters the previous Key Vault resource will remain in the resource group but will no longer be managed by the stack. Instead, the new resource will now be (deployed) and tracked by the stack.

azcli
az stack sub create \
--name 'az-stack-uks-demo' \
--location 'uksouth' \
--template-file 'main.bicep' \
--parameters 'main.bicepparam' \
--action-on-unmanage 'detachAll' \
--deny-settings-mode 'denyWriteAndDelete' \
--deny-settings-apply-to-child-scopes
PowerShell

deleteResources

Following on from above, defining deleteResources means the deployment stack will delete the Azure resource if it becomes unmanaged. For example, if I accidentally deploy a storage account with the wrong name, I can update my parameter or variable to reflect the correct name. Upon deployment, the stack will delete the original storage account and create the new one, managing it within the deployment stack. This behaviour applies to any resource tracked by the stack, but only for resources within the resource group (and not the resource group itself in this mode).

The deployment stack will make multiple attempts (retries) to delete a resource in a best effort approach. However, some resources simply cannot be deleted. For instance, removing a Data Collection Rule resource from your Bicep template will fail if it has associated VMs, due to external resource dependencies. This type of scenario applies to other resources with similar dependencies on linked resources:

In addition, if you want to increase a subnet size with connected resources stacks also seems to struggle. So it does have some nuances with the deleteResources mode that hopefully will improve in the near future.

For the most part the clean-ups should work nicely, and you’ll be able to review what resources have been deleted from the last deployment in the stacks management overview:

The deployment stack overview shows the deleted resources:

azcli
az stack sub create \
--name 'az-stack-uks-demo' \
--location 'uksouth' \
--template-file 'main.bicep' \
--parameters 'main.bicepparam' \
--action-on-unmanage 'deleteResources' \
--deny-settings-mode 'denyWriteAndDelete' \
--deny-settings-apply-to-child-scopes
PowerShell

deleteAll

The same as deleteResources, except the deleteAll setting will also remove resource groups themselves, if the stack is managing them. For example, I delete the resource group(s) resource from my bicep file, on stack deployment it’s going to delete the resource group(s) including all the resources.

azcli
az stack sub create \
--name 'az-stack-uks-demo' \
--location 'uksouth' \
--template-file 'main.bicep' \
--parameters 'main.bicepparam' \
--action-on-unmanage 'deleteAll' \
--deny-settings-mode 'denyWriteAndDelete' \
--deny-settings-apply-to-child-scopes
PowerShell

A quick way to visually view the different unmanage options and what they will do is in the portal, when you click ‘delete stack’:

Deny Settings

In deployment stacks you can also define deny settings. These deny options help prevent resource and property changes that you do not authorise and intend to protect as part of the stack management for the resources.

Deny settings only work on explicitly created resources as part of the deployment stack. If a user or principal creates a resource within in the portal, it will not be managed by the stack and therefore not subject to deny settings. They also do not apply to data plane actions.

The documentation does a really great job at explaining the different deny settings and their consequences. So in the interest of not repeating them in my own words, here’s a quote directly from the docs:

  • deny-settings-apply-to-child-scopes: When specified, the deny setting mode configuration also applies to the child scope of the managed resources. For example, a Bicep file defines a Microsoft.Sql/servers resource (parent) and a Microsoft.Sql/servers/databases resource (child). If a deployment stack is created using the Bicep file with the deny-settings-apply-to-child-scopes setting enabled and the deny-settings-mode set to denyWriteAndDelete, you can’t add any additional child resources to either the Microsoft.Sql/servers resource or the Microsoft.Sql/servers/databases resource.
  • deny-settings-excluded-actions: List of RBAC management operations excluded from the deny settings. Up to 200 actions are allowed.
  • deny-settings-excluded-principals: List of Microsoft Entra principal IDs excluded from the lock. Up to five principals are allowed.

Here’s some observations to call out about deny settings from my experiences so far are:

  • If you have deployIfNotExist (DINE) Azure Policies then when need to remediate resource properties where a deny setting is set to DenyWriteAndDelete it will get blocked and not remediate. By defining the remediation principals objectId you can exclude it from the deny settings using the deny-settings-excluded-principals switch but this is likely not very viable for a lot of tenants.
  • You can review what principals are excluded from the deny settings by reviewing the deployment stack in the Portal:
  • As above from the documentation quote, if you do not include the deny-settings-apply-to-child-scopes then child scopes are subject to modifications and deletions. For example, a network security group rule can be deleted if you do not specify this deny setting. This may be a consideration for some of your stacks where you wish teams to be able to manage rules, but not modify the parent resource itself.
  • Consider GitOps and other DevOps processes when using deny settings. For example, if you leverage APIOps for API Management API lifecycle then a DenyWriteAndDelete setting may block this process unless you exclude the pipeline principal as part of the excluded principals switch.

You can exclude objectIds from the deployment stacks deny settings (only up to a maximum of 5):

azcli
az stack sub create \
....
--deny-settings-excluded-principals '8ce39efa-ec33-4ed7-9c3a-07521316e953 ... '
PowerShell

However you can also exclude RBAC actions from the deny settings too, and you can specify up to 200

azcli
az stack sub create \
....
--deny-settings-excluded-actions 'Microsoft.Consumption/budgets/delete'
PowerShell
Resources deployed outside of your Bicep template (and therefore not managed by the deployment stack) are NOT subject to stack management (deny/deletes)!

Outputs

An interesting part of the stack metadata is it will show your Bicep templates outputs in the stack overview:

This could be useful in the future as a feature by allowing users to reference an existing stack output for things like a resourceId or principal, similar to how you can reference state file outputs in Terraform.

Adoption

If you’re already deploying Bicep (or ARM templates), adoption will likely be far more straightforward than you might think. There’s no need to make any changes to your Bicep templates to prepare them for stacks. This is because Deployment Stacks are essentially an extension built on top of the existing deployment mechanisms we use today for Azure deployments.

With this in mind, adoption is more about updating your deployment scripts, pipelines, or similar processes to use the Deployment Stacks commands, actions, or tasks. You’ll then be able to bring your existing Bicep templates and, therefore, the resources they deploy under Deployment Stacks management, using your defined modes.

TIP: Consider adding a hidden-title tag into your Azure tag deployment so you can easily track which resources are managed by deployment stacks and which are not (yet) adopted or (deployed) outside of a stack.
FROM THIS:
az deployment sub create -l uksouth -f main.bicep -p main.bicepparam

New-AzSubscriptionDeployment -Location uksouth -TemplateFile main.bicep -TemplateParameterFile main.bicepparam

TO THIS:
az stack sub create --name 'azstack' --location 'uksouth' --template-file 'main.bicep' --parameters 'main.bicepparam' --action-on-unmanage 'deleteResources' --deny-settings-mode 'denyWriteAndDelete' --deny-settings-apply-to-child-scopes

New-AzSubscriptionDeploymentStack -Name "azstack" -Location "uksouth" -TemplateFile "main.bicep" -ActionOnUmanage "detachAll" -DenySettingsMode "none"
PowerShell

I would recommend deploying your stacks at least at the subscription level for better flexibility and to allow control of multiple resource groups if required. In addition, it keeps the stack settings out of reach for teams who may have RBAC on the resource groups enforcing your stack modes.

GitHub

The (somewhat) new Bicep Deploy GitHub Action already has good support for Deployment stacks – making CI/CD in GitHub very painless to shift from your current setup.

Bicep Deploy · Actions · GitHub Marketplace

      - name: Deployment
        uses: azure/[email protected]
        with:
          type: deploymentStack
          operation: create
          name: riosengineer
          location: uksouth
          scope: subscription
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          template-file: ./main.bicep
          parameters-file: ./main.bicepparam
          action-on-unmanage-resources: delete
          deny-settings-mode: denyWriteAndDelete
YAML

With some more examples here: bicep-deploy/examples/STACKS.md at main · Azure/bicep-deploy

Azure DevOps

Azure DevOps users unfortunately don’t get a fancy task to leverage like the above GitHub Action unfortunately. And the current ones don’t (yet) support deployment stacks. So for now, you’ll need to adjust your pipeline YAMLs to deploy your Bicep templates using the stack cmdlets in order for these to change (PowerShell or Azure CLI tasks).

Limitations

Straight from the Microsoft documentation:

  • Implicitly created resources aren’t managed by deployment stack. Therefore, no deny-assignments or cleanup is possible.
  • Deny-assignments don’t support tags.
  • Deny-assignments aren’t supported at the management group scope. However, they’re supported in a management group stack if the deployment is pointed at the subscription scope.
  • Deployment stacks can’t delete Key vault secrets. If you’re removing key vault secrets from a template, make sure to also execute the deployment stack update/delete command with detach mode.

Be sure to also review the ‘known issues’ section as well.

What-if is coming to stacks very soon and should include the latest what-if fixes which help fix noise reduction (check the latest comments here.)

Try it out!

I’ve put together a really simple Bicep template, if you just want to quickly copy/paste and try things out for yourself:

// az stack sub create –name 'az-stack-uks-demo' –location 'uksouth' –template-file 'main.bicep' –parameters 'main.bicepparam' –action-on-unmanage 'deleteResources' –deny-settings-mode 'denyWriteAndDelete' –deny-settings-apply-to-child-scopes –deny-settings-excluded-principals '
targetScope = 'subscription'
// Metadata
metadata name = 'Deployment Stacks'
metadata description = 'Zero to Hero – Bicep with Deployment Stacks'
metadata owner = '[email protected]'
// MARK: Parameters
@description('Azure region to deploy resources.')
param location string
@description('Tags to apply to resources.')
param tags object
@description('Address prefix for the virtual network.')
param vnetAddressPrefix array
@description('Address prefix for the subnet.')
param snetAddressPrefix string
// MARK: Variables
var rgName = 'rg-${toLower(substring(location, 0, 3))}-stack-demo'
var storageName = 'st${toLower(substring(location, 0, 3))}stackdemo001'
var storageName2 = 'st${toLower(substring(location, 0, 3))}stackdemo002'
var nsgName = 'nsg-${toLower(substring(location, 0, 3))}-stack-demo'
var nsgName2 = 'nsg-${toLower(substring(location, 0, 3))}-stack-demo2'
var vnetName = 'vnet-${toLower(substring(location, 0, 3))}-stack-demo'
var snetName = 'snet-${toLower(substring(location, 0, 3))}-stack-demo'
// Modules
// MARK: Resource Group
module modResourceGroup 'br/public:avm/res/resources/resource-group:0.4.1' = {
name: '${uniqueString(deployment().name, location)}-${rgName}'
params: {
name: rgName
location: location
tags: tags
}
}
// MARK: Storage Account
module modStorageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
scope: resourceGroup(rgName)
name: '${uniqueString(deployment().name, location)}-${storageName}'
params: {
name: storageName
location: location
kind: 'StorageV2'
allowSharedKeyAccess: false
allowBlobPublicAccess: true
minimumTlsVersion: 'TLS1_2'
accessTier: 'Hot'
publicNetworkAccess: 'Enabled'
blobServices:{
containers: [
{
name: 'stacks'
}
]
}
managedIdentities: {
systemAssigned: true
}
tags: tags
}
dependsOn: [
modResourceGroup
]
}
// Comment out if and redeploy the stack to see deleteResources action
// MARK: Storage Account2
module modStorageAccount2 'br/public:avm/res/storage/storage-account:0.19.0' = {
scope: resourceGroup(rgName)
name: '${uniqueString(deployment().name, location)}-${storageName2}'
params: {
name: storageName2
location: location
kind: 'StorageV2'
allowSharedKeyAccess: false
allowBlobPublicAccess: true
minimumTlsVersion: 'TLS1_2'
accessTier: 'Hot'
publicNetworkAccess: 'Enabled'
blobServices:{
containers: [
{
name: 'stacks'
}
]
}
managedIdentities: {
systemAssigned: true
}
tags: tags
}
dependsOn: [
modResourceGroup
]
}
// MARK: Network Security Group
module modNsg 'br/public:avm/res/network/network-security-group:0.5.1' = {
scope: resourceGroup(rgName)
name: '${uniqueString(deployment().name, location)}-${nsgName}'
params: {
name: nsgName
location: location
securityRules: [
{
name: 'Deny-RDP-Internet'
properties: {
priority: 100
access: 'Deny'
direction: 'Inbound'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '3389'
sourceAddressPrefix: 'Internet'
destinationAddressPrefix: '*'
}
}
]
tags: tags
}
dependsOn: [
modResourceGroup
]
}
// MARK: Network Security Group
module modNsg2 'br/public:avm/res/network/network-security-group:0.5.1' = {
scope: resourceGroup(rgName)
name: '${uniqueString(deployment().name, location)}-${nsgName2}'
params: {
name: nsgName2
location: location
securityRules: [
{
name: 'Deny-RDP-Internet'
properties: {
priority: 100
access: 'Deny'
direction: 'Inbound'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '3389'
sourceAddressPrefix: 'Internet'
destinationAddressPrefix: '*'
}
}
]
tags: tags
}
dependsOn: [
modResourceGroup
]
}
// MARK: Virtual Network
module modVirtualnetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
scope: resourceGroup(rgName)
name: '${uniqueString(deployment().name, location)}-${vnetName}'
params: {
name: vnetName
addressPrefixes: vnetAddressPrefix
subnets: [
{
name: snetName
addressPrefix: snetAddressPrefix
networkSecurityGroupResourceId: modNsg.outputs.resourceId
}
]
}
dependsOn: [
modResourceGroup
]
}
// Outputs
output storageAccountId string = modStorageAccount.outputs.resourceId
output virtualNetworkName string = modVirtualnetwork.outputs.name
output storageAccountName string = modStorageAccount.outputs.name
output nsgName string = modNsg.outputs.name
view raw main.bicep hosted with ❤ by GitHub
using 'main.bicep'
param location = 'uksouth'
param tags = {
Environment: 'demo'
Project: 'Deployment Stacks'
'hidden-title': 'Managed'
}
param vnetAddressPrefix = [
'10.0.0.0/24'
]
param snetAddressPrefix = '10.0.0.0/28'
// az bicep upgrade
// az stack sub create –name 'az-stack-uks-demo' –location 'uksouth' –template-file 'main.bicep' –parameters 'main.bicepparam' –action-on-unmanage 'deleteResources' –deny-settings-mode 'denyWriteAndDelete' –deny-settings-apply-to-child-scopes
view raw main.bicepparam hosted with ❤ by GitHub

Conclusion

Thanks if you made it this far, and I trust the blog had useful insights into Deployment Stacks! It’s the future of Bicep deployments and so it’s critical you look to try and adopt it as your go to deployment and management mechanism for your Azure resources! I’m excited how stacks will evolve and keen to see how what-if enhances the robustness of the tooling, which should be coming any month now.

What’s your thoughts on stacks so far? Are you using it? Enjoying it? Think it has some gaps?

Leave a comment


Skip to content