Plain az deployment group create is incremental: it creates and updates, but never deletes what you removed from the template. Over a year that leaves orphaned resources and silent drift. Two features fix most of it: what-if to review changes before they happen, and deployment stacks to own a set of resources as one unit.
What-if as a gate, not a curiosity
az deployment group what-if -g rg-app-prod -f main.bicep -p main.prod.bicepparam \
--result-format FullResourcePayloads
The change types to read: Create, Delete (only shown in complete mode or stacks), Modify, NoChange, Ignore, Deploy (cannot predict). In a pipeline, fail when risky changes appear:
az deployment group what-if -g rg-app-prod -f main.bicep -p main.prod.bicepparam \
--no-pretty-print > whatif.json
jq -e '[.changes[] | select(.changeType=="Delete" or (.changeType=="Modify" and (.resourceId|test("Microsoft.Network|Microsoft.KeyVault"))))] | length == 0' whatif.json \
|| { echo "Risky change: needs manual approval"; exit 1; }
Warning
What-if has noise: some properties always show as
Modify(read-only values, defaults the provider fills in). Keep a short allow-list of known-noisy properties rather than ignoring what-if altogether.
Deployment stacks: own the lifecycle
A stack is an Azure resource that remembers which resources a deployment manages. When you remove a resource from the template and redeploy, the stack either detaches it (leaves it, unmanaged) or deletes it.
az stack group create \
--name stk-app-prod -g rg-app-prod \
--template-file main.bicep --parameters main.prod.bicepparam \
--action-on-unmanage detachAll \
--deny-settings-mode none
--action-on-unmanage values: detachAll, deleteResources, deleteAll (also deletes resource groups the stack manages). Parameter names have changed between CLI versions; confirm with az stack group create --help.
Recommended progression:
- Create stacks with
detachAllfor existing estates. Nothing gets deleted; you just start tracking. - Review
az stack group show -n stk-app-prod -g rg-app-prod --query resourcesand compare with the resource group. - Move to
deleteResourcesonce the template truly describes the workload.
az stack group list -g rg-app-prod -o table
az stack group show -n stk-app-prod -g rg-app-prod --query "resources[].id" -o tsv
Caution
With
deleteResources, a template refactor that renames a resource (new symbolic name and new resource name) deletes the old one. For stateful resources (databases, storage, Key Vault) run what-if, and add resource locks as a second safety net.
Deny settings: stop portal drift
Deny settings put a deny assignment on stack-managed resources so changes must go through the stack.
az stack group create --name stk-app-prod -g rg-app-prod \
--template-file main.bicep --parameters main.prod.bicepparam \
--action-on-unmanage detachAll \
--deny-settings-mode denyWriteAndDelete \
--deny-settings-excluded-principals <pipeline-sp-object-id> <breakglass-group-object-id> \
--deny-settings-apply-to-child-scopes
| Mode | Effect |
|---|---|
none | Tracking only |
denyDelete | Changes allowed, deletes blocked |
denyWriteAndDelete | Only excluded principals or the stack itself can change resources |
Some actions are needed by the platform (for example key rotation, scaling by an autoscaler). Add them with --deny-settings-excluded-actions rather than dropping the deny mode.
Subscription and management group stacks
Landing-zone resources (policy assignments, role assignments, resource groups) fit stacks at higher scopes:
az stack sub create --name stk-platform --location westeurope \
--template-file platform.bicep --action-on-unmanage detachAll --deny-settings-mode denyDelete
az stack mg create --name stk-policy --management-group-id mg-corp --location westeurope \
--template-file policy.bicep --action-on-unmanage detachAll --deny-settings-mode none
Pipeline shape
bicep buildandbicep lint(fail on warnings you care about).- What-if against the target; publish the result as an artifact.
- Gate: auto-approve when there are no deletes or network/identity modifications; otherwise manual approval.
az stack group createwith the same parameters.- Post-deploy:
az stack group showand compare resource count with the previous run.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
DenyAssignmentAuthorizationFailed on an emergency change | Deny settings active | Use an excluded break-glass principal, or set --deny-settings-mode none temporarily via the stack |
| Resource unexpectedly deleted | Renamed in template with deleteResources | Restore from soft delete or backup; switch to detachAll while refactoring |
| Stack update fails on a resource others manage | Two stacks claim the same resource | One owner per resource; detach from one stack |
Prevent it next time
- Every production deployment runs what-if first and stores the result.
- Stacks with deny settings for production resource groups; break-glass group excluded and monitored.
- Locks plus soft delete on stateful resources, so a template mistake is recoverable.