Home / Azure

AzureHow-toAdvanced

Bicep at scale: what-if gates, deployment stacks and deny settings to stop drift and orphaned resources

Use what-if output as a pipeline gate, manage resource lifecycles with deployment stacks so removed resources are actually deleted or detached, and protect stack-managed resources from portal edits with deny settings.

Deepak Bhardwaj4 min read

Applies toAzure Bicep, ARM deployments, Azure deployment stacks (Azure CLI 2.61+ / Az PowerShell; check current docs for feature status per scope)

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:

  1. Create stacks with detachAll for existing estates. Nothing gets deleted; you just start tracking.
  2. Review az stack group show -n stk-app-prod -g rg-app-prod --query resources and compare with the resource group.
  3. Move to deleteResources once 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

  1. bicep build and bicep lint (fail on warnings you care about).
  2. What-if against the target; publish the result as an artifact.
  3. Gate: auto-approve when there are no deletes or network/identity modifications; otherwise manual approval.
  4. az stack group create with the same parameters.
  5. Post-deploy: az stack group show and 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.