Azure Deployment Stacks Change Everything About Resource Lifecycle
How Deployment Stacks solve the resource orphan problem that Bicep and ARM alone can't — with DenySettings, managed cleanup, and lifecycle awareness.
The Problem Nobody Talks About
Here's a scenario I've seen break in client environments more times than I'd like to admit.
You write a Bicep template. It deploys a storage account, an app service plan, and a key vault. Everything works. Three sprints later, you remove the key vault from your template because the architecture changed. You redeploy.
The key vault is still there.
Bicep deployed it. Bicep didn't delete it. And Bicep never will. That's not what it does. ARM template deployments are additive by design -- they create and update resources, but they don't remove resources that disappear from the template.
Over six months, a typical project accumulates dozens of orphaned resources. Storage accounts nobody owns. NICs attached to nothing. App service plans running at Basic tier for no reason. The monthly bill creeps up, and nobody knows why.
This is the lifecycle management gap. And until recently, Azure had no native answer for it.
What Deployment Stacks Actually Are
A deployment stack is a native Azure resource (type Microsoft.Resources/deploymentStacks) that wraps your Bicep or ARM deployment and tracks every resource it creates. Think of it as a manifest that knows what belongs to your deployment and what doesn't.
The stack sits at a specific scope -- resource group, subscription, or management group -- and maintains a list of managed resources. When you update the stack with a modified template, it compares the new resource list against the old one and can automatically clean up anything that was removed.
This is not Terraform state. It's lighter than that. The stack doesn't store resource configurations -- it stores resource IDs. Azure itself remains the source of truth for resource properties. The stack just knows what it owns.
Three scope levels are supported:
- Resource group scope (
az stack group) -- manages resources within a single resource group - Subscription scope (
az stack sub) -- can manage resources and resource groups - Management group scope (
az stack mg) -- manages across subscriptions

CloudLearn Lab: Bicep Deployment Stacks Lifecycle Management
Practice create, update, protection, and cleanup workflows with Deployment Stacks in Azure.
Creating Your First Deployment Stack
Let's walk through this end to end. First, here's a simple Bicep template that deploys a storage account and an app service plan.
The Bicep Template
// main.bicep
@description('Azure region for all resources')
param location string = resourceGroup().location
@description('Unique suffix for resource names')
param nameSuffix string = uniqueString(resourceGroup().id)
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: 'ststack${nameSuffix}'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: 'asp-stack-${nameSuffix}'
location: location
sku: {
name: 'B1'
tier: 'Basic'
}
kind: 'linux'
properties: {
reserved: true
}
}
output storageAccountName string = storageAccount.name
output appServicePlanName string = appServicePlan.nameNothing fancy. Two resources, parameterized names, outputs for verification.
Creating the Stack
# Create a resource group first
az group create \
--name rg-deployment-stacks-demo \
--location eastus
# Create the deployment stack
az stack group create \
--name my-app-stack \
--resource-group rg-deployment-stacks-demo \
--template-file main.bicep \
--action-on-unmanage deleteResources \
--deny-settings-mode none \
--yesTwo flags that matter here:
--action-on-unmanage deleteResourcestells the stack to delete any managed resources that get removed from the template on the next update. This is the killer feature. The alternative values aredetachAll(just stop tracking them) anddeleteAll(delete resources AND resource groups).--deny-settings-mode nonemeans no protection on the resources yet. We'll add that next.
Verify What the Stack Manages
# Show stack details
az stack group show \
--name my-app-stack \
--resource-group rg-deployment-stacks-demo \
--output table
# List all stacks in the resource group
az stack group list \
--resource-group rg-deployment-stacks-demo \
--output tableThe output shows you every resource the stack is tracking. This is your source of truth for what's managed.
DenySettings: Protecting Your Resources
This is honestly one of the most underappreciated features. DenySettings let you put a lock on every resource the stack manages -- without configuring individual resource locks.
Three Modes
| Mode | What It Does | When to Use It |
|---|---|---|
none | No protection | Dev/test environments |
denyDelete | Blocks deletion of managed resources | Staging, shared environments |
denyWriteAndDelete | Blocks writes AND deletion | Production critical infrastructure |
Applying DenySettings
# Create a stack with delete protection
az stack group create \
--name my-app-stack \
--resource-group rg-deployment-stacks-demo \
--template-file main.bicep \
--action-on-unmanage deleteResources \
--deny-settings-mode denyDelete \
--yesNow try deleting the storage account manually. Azure will block it with a deny assignment. Not an RBAC role -- a deny assignment. These override even Owner permissions.
Excluding Specific Actions or Principals
Sometimes you need to allow certain operations even with deny settings on. Maybe your CI/CD service principal needs write access, or you want to allow tag updates.
# Deny deletes but allow a specific principal and action
az stack group create \
--name my-app-stack \
--resource-group rg-deployment-stacks-demo \
--template-file main.bicep \
--action-on-unmanage deleteResources \
--deny-settings-mode denyDelete \
--deny-settings-excluded-principals "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--deny-settings-excluded-actions "Microsoft.Storage/storageAccounts/write" \
--yesYou can exclude up to 5 principals and up to 200 actions. That's generous enough for most production setups.
Apply to Child Scopes
# Extend deny settings to child scopes
az stack group create \
--name my-app-stack \
--resource-group rg-deployment-stacks-demo \
--template-file main.bicep \
--action-on-unmanage deleteResources \
--deny-settings-mode denyWriteAndDelete \
--deny-settings-apply-to-child-scopes \
--yesThe --deny-settings-apply-to-child-scopes flag propagates protection down. Super important if you're deploying resources that have their own child resources, like a storage account with blob containers.
The Killer Feature: Managed Cleanup
This is what you came here for. Let's see it in action.
Step 1: Remove a Resource from the Template
Edit your main.bicep and remove the app service plan entirely:
// main.bicep (updated -- app service plan removed)
@description('Azure region for all resources')
param location string = resourceGroup().location
@description('Unique suffix for resource names')
param nameSuffix string = uniqueString(resourceGroup().id)
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: 'ststack${nameSuffix}'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
output storageAccountName string = storageAccount.nameStep 2: Update the Stack
# Update the stack with the modified template
az stack group create \
--name my-app-stack \
--resource-group rg-deployment-stacks-demo \
--template-file main.bicep \
--action-on-unmanage deleteResources \
--deny-settings-mode denyDelete \
--yesYes, you use az stack group create to update too. It's an upsert operation.
What Happens
Because we set --action-on-unmanage deleteResources, the stack:
- Detects that the app service plan is no longer in the template
- Removes it from the managed resources list
- Deletes the actual app service plan from Azure
No orphan. No manual cleanup. No forgotten resource burning budget.
If you'd used --action-on-unmanage detachAll instead, the stack would stop tracking the app service plan but leave the resource alive. That's useful when you're moving a resource to a different stack.
Deleting the Entire Stack
# Delete the stack and all its managed resources
az stack group delete \
--name my-app-stack \
--resource-group rg-deployment-stacks-demo \
--action-on-unmanage deleteResources \
--yesGone. The stack, the storage account, everything it managed. Clean.
Updating a Stack: What Really Happens
When you run az stack group create against an existing stack, the behavior depends on your --action-on-unmanage setting:
| Scenario | deleteResources | deleteAll | detachAll |
|---|---|---|---|
| Resource removed from template | Deleted | Deleted | Detached (orphaned) |
| Resource group removed from template | Kept (empty) | Deleted | Detached |
| Resource added to template | Created | Created | Created |
| Resource modified in template | Updated | Updated | Updated |
The deleteResources vs deleteAll distinction matters at subscription scope. If your stack created a resource group and you remove it from the template, deleteResources deletes the resources inside but keeps the empty resource group. deleteAll removes the resource group itself.
Plain Bicep vs Deployment Stacks
| Capability | Plain Bicep Deploy | Deployment Stacks |
|---|---|---|
| Create resources | Yes | Yes |
| Update resources | Yes | Yes |
| Delete removed resources | No | Yes |
| Resource lifecycle tracking | No | Yes |
| Deny assignments (lock resources) | No (manual locks only) | Built-in |
| Orphaned resource prevention | No | Yes |
| Scope awareness | Resource group only | RG, Sub, or MG |
| State file | None | Azure-managed (resource IDs) |
| CI/CD integration | Direct | Same commands |
| Rollback | Redeploy previous template | Redeploy previous template |
The truth is, Deployment Stacks don't replace Bicep. They wrap Bicep. You still write the same templates, the same modules, the same parameters. The stack just adds lifecycle awareness on top.
Gotchas and Limitations
I've been honest about the value. Now let me be honest about the rough edges.
1. Implicitly created resources are not managed. If your Bicep template deploys a VM and Azure automatically creates a managed disk for it, that disk is not tracked by the stack. You can't protect it with deny settings or clean it up automatically. This is a meaningful gap.
2. 800 stacks per scope limit. You can have a maximum of 800 deployment stacks at any given scope. For most teams this is fine, but if you're doing micro-stacks per feature team per environment, plan accordingly.
3. 2,000 deny assignments per scope. Each stack with deny settings creates deny assignments. The limit is 2,000 per scope. In large environments with many stacks and many resources, you can hit this.
4. Microsoft Graph resources are not supported. If you're deploying Entra ID (Azure AD) resources through Microsoft Graph, deployment stacks won't help you. The Graph provider doesn't support stacks.
5. Deny assignments don't support tags. You can't tag deny assignments. This makes it harder to audit which deny assignments belong to which stack in the portal, though the CLI handles this fine.
6. The "create" command is also the "update" command.
az stack group create is an upsert. This is convenient but can confuse CI/CD pipelines that expect separate create and update paths. Just know that running create against an existing stack updates it.
7. Budget resources can be tricky.
There are known issues where denyWriteAndDelete doesn't fully protect budget resources at the subscription level. If you're relying on stacks to protect budgets, test thoroughly.
8. Error messages can be vague. When template validation fails through a stack, the error message is sometimes less helpful than a direct Bicep deployment error. If you hit a confusing error, try deploying the template directly first to get a better message, then wrap it in a stack.
When to Use Deployment Stacks vs Plain Bicep
Use Deployment Stacks when:
- You need lifecycle management (automatic cleanup of removed resources)
- You want resource protection without managing individual locks
- Your team frequently modifies templates and resources come and go
- You're managing shared environments where accidental deletion is a real risk
- You want a single "delete everything" command for environment teardown
Stick with plain Bicep deployments when:
- You're doing one-time deployments that won't change
- You need to deploy Microsoft Graph / Entra ID resources
- Your templates are stable and resources rarely get removed
- You're already using Terraform or Pulumi for lifecycle management
My recommendation: if you're building anything that will live longer than a demo, use Deployment Stacks. The overhead is one extra flag on your deploy command. The payoff is zero orphaned resources and built-in protection.
This connects directly to the Bicep fundamentals covered in our CloudLearn labs. If you've gone through the IaC module, Deployment Stacks are the natural next step -- they're what completes the full infrastructure lifecycle story in Azure.
Prerequisites to try this yourself:
- Azure CLI version 2.61.0 or later
- An active Azure subscription
- Contributor role (minimum) on the target resource group
# Verify your CLI version supports deployment stacks
az version --output table
# If you need to update
az upgradeRead Next
Getting Started with Terraform on Azure — Deploy Your First Resource
Learn the basics of Terraform on Azure by writing a configuration file and deploying a storage account. Covers providers, variables, data sources, and the init-plan-apply workflow.
Infrastructure as Code: Why ARM Templates Are Still Worth Learning in 2025
Bicep and Terraform get all the hype, but understanding ARM templates will make you a better cloud engineer.