← All posts
azurebicepiacgovernance

Azure Deployment Stacks: Bicep's missing lifecycle layer

Plain Bicep deployments leave behind resources you remove from a template. Deployment Stacks fix that gap with automatic cleanup and drift protection — here is how they work in practice.

Remove a resource from a Bicep template and redeploy — what happens to the Azure resource? Nothing. It stays where it is, quietly accumulating cost and permissions, until someone manually tracks it down and deletes it. On a team, with dozens of environments, that someone often never materialises.

Deployment Stacks are Azure’s answer to this problem. Generally available since 2024, they wrap your Bicep (or ARM) template in a management layer that tracks everything that was deployed and gives you real control over what happens when something leaves the template. If you have been using Terraform and want equivalent lifecycle management in native Azure tooling, this is the feature you have been waiting for.

What a deployment stack actually is

A deployment stack is a Microsoft.Resources/deploymentStacks ARM resource. You deploy it the same way as any other ARM resource — using the Azure CLI, Azure PowerShell, or the portal — but instead of deploying resources directly, it wraps your Bicep file and deploys through it. The stack then tracks every resource that file creates.

That tracking is the key insight: the stack maintains an explicit list of its managed resources. When you redeploy the stack with an updated template, anything no longer in the template becomes unmanaged, and you decide what happens to it.

The two controls that matter

Every stack creation or update command has two parameters you need to think about deliberately.

actionOnUnmanage — what happens to resources that fall out of the template:

  • detachAll — the resource stays in Azure, just no longer tracked by the stack
  • deleteResources — managed resources (but not resource groups) are deleted
  • deleteAll — both managed resources and managed resource groups are deleted

denySettings — protection against out-of-band changes while the stack is active:

  • none — no protection (identical to a plain deployment)
  • denyDelete — managed resources cannot be deleted outside the stack
  • denyWriteAndDelete — no writes or deletes on managed resources, enforced via Azure deny assignments

The combination you choose depends on how strict you need to be. For a shared networking stack managed by a platform team, I would typically pair deleteResources with denyDelete — clean up what leaves the template, and protect what stays. For a sandbox environment meant to be wiped after use, deleteAll on teardown makes cleanup a one-command operation.

Creating a stack

Creating a stack at the subscription scope, deploying resources across multiple resource groups:

az stack sub create \
  --name 'platform-connectivity' \
  --location 'westeurope' \
  --template-file './main.bicep' \
  --action-on-unmanage 'deleteResources' \
  --deny-settings-mode 'denyDelete'

Or with Azure PowerShell:

New-AzSubscriptionDeploymentStack `
  -Name 'platform-connectivity' `
  -Location 'westeurope' `
  -TemplateFile './main.bicep' `
  -ActionOnUnmanage 'deleteResources' `
  -DenySettingsMode 'denyDelete'

Stacks also work at resource group scope (az stack group create / New-AzResourceGroupDeploymentStack) and management group scope for policy-layer resources. Minimum versions required: Azure PowerShell 12.0.0 or Azure CLI 2.61.0.

The cleanup lifecycle in practice

When you remove a resource from your Bicep file and redeploy, the stack computes the diff between what is now in the template and what it was previously tracking. Resources that fall out of that set are handled according to your actionOnUnmanage setting — no manual hunting required.

This matters most for ephemeral workloads: developer sandboxes, test environments, project-scoped resources with a defined end date. Instead of relying on someone to remember to clean up, the stack teardown handles it:

az stack sub delete \
  --name 'sandbox-dev' \
  --action-on-unmanage 'deleteAll'

One command, and every resource the template ever deployed is gone. Compared to assembling a deletion list manually — or worse, running az resource list and deleting in a loop hoping you got everything — this is a different category of confidence.

Deny settings and drift protection

In platform team contexts, one of the harder problems is resources modified outside the pipeline. Someone applies a quick change in the portal “just this once” and now your template diverges from reality. The denyDelete and denyWriteAndDelete modes address this by creating Azure deny assignments on every managed resource.

For resources that a separate team needs to manage independently — a Key Vault, a Log Analytics workspace — you can carve out exceptions using --deny-settings-excluded-principals (by Entra object ID) or --deny-settings-excluded-resources (by resource ID). The deny assignments are created under the hood by Azure, not by you, and they count against the 2,000 deny assignment limit per scope.

Limitations worth knowing about

A few edges to be aware of before committing stacks to production:

  • 800 stacks per scope is the hard limit. Rarely a problem, but scope your stacks to match the management lifecycle of the resources, not for organisational convenience.
  • Key Vault secrets are not deleted by stack cleanup, even with deleteAll. Remove secrets manually or via the pipeline before tearing down a Key Vault managed by a stack.
  • denyWriteAndDelete is aggressive. It blocks even legitimate changes unless the principal is excluded. Start with denyDelete and escalate only where you need it.
  • No What-if support yet. You cannot preview a stack update the way you can a regular Bicep deployment. Test against a non-production environment first.
  • Management group-scoped stacks can only deploy into their own management group or child subscriptions, not into a peer management group.

The takeaway

Deployment Stacks fill a genuine gap in native Azure IaC tooling. If your Bicep workflows have a persistent resource-drift or cleanup problem, they are worth the small learning curve. Start at resource group scope with a non-critical workload, use detachAll on unmanage while you learn the behaviour, then tighten to deleteResources once you are confident the template is the source of truth. The deny settings layer is a separate concern — useful, but independent of the cleanup story.


Sources & further reading: Create and deploy Azure deployment stacks in Bicep, Quickstart: Create a deployment stack with Bicep, Azure deployment stacks integration with Azure Developer CLI, ARM Deployment Stacks now GA.