Cloud

Azure Deployment Stacks: validate unmanage and deny settings before an update

A production runbook for reviewing managed resources, unmanage behavior, deny settings, excluded identities, validation and rollback before updating an Azure Deployment Stack.

31 Jul 2026 azuredeployment-stacksbicepiacautomationguardrailsrbacgovernancerunbookrollbackproduction

An Azure Deployment Stack update can deploy the intended template and still create the wrong operational outcome. The risk appears when a resource leaves the template, when unmanage behavior does not match the team’s intent, or when a deny setting prevents operations staff, a pipeline identity or a recovery tool from acting later.

The use case is a platform-prod stack that manages an application’s foundation: networking, identity, secrets, observability and a few shared resources. A pull request removes a resource that appears obsolete and tightens deny settings. Before promotion, the team must decide whether the stack can be updated, whether some resources should be detached, whether the template or deny scope needs correction, or whether the change must stay blocked until recovery is proven.

Freeze The Stack Contract

Start by writing down the intended boundary. A stack is more than a named Bicep template. It also carries an ownership boundary, a lifecycle decision for resources that leave that boundary, and protection rules that can affect operations after deployment.

yaml deployment-stack-change-contract.yml
stack:
name: platform-prod
scope: resource-group
resource_group: rg-platform-prod
template_ref: platform/main.bicep@8f42c31
pipeline_identity: mi-platform-deployer-prod

lifecycle_intent:
removed_managed_resources: detach_until_owner_confirms
shared_resources: never_delete_from_this_stack
application_resources: delete_only_with_data_owner_approval

protection_intent:
mode: denyDelete
apply_to_child_scopes: false
excluded_principals:
  - mi-platform-deployer-prod
  - breakglass-platform-operations

promotion_blockers:
- managed_resource_inventory_missing
- unmanage_action_not_reviewed
- excluded_principal_not_tested
- restore_or_recreate_path_missing
- post_change_probe_missing

This contract removes two ambiguities: assuming that a resource absent from the new template should disappear, and treating a deny setting as a minor RBAC detail. Both decisions directly affect operability.

Capture Effective State Before The Diff

The repository describes intent. Azure describes reality. Capture the stack before the change, including provisioning state, protection settings and managed resources. Keep the export with deployment evidence.

bash 01-capture-current-stack.sh
az stack group show --resource-group rg-platform-prod --name platform-prod --output json > platform-prod.before.json

az stack group show --resource-group rg-platform-prod --name platform-prod --query "{state:provisioningState,actionOnUnmanage:actionOnUnmanage,denySettings:denySettings,resources:resources}" --output json

Review the result instead of trusting the last green pipeline. A resource may have moved, been recreated under a different ID or changed outside IaC. A stack in an unexpected state is not a safe baseline for a destructive update.

Build A Managed Resource Inventory

The useful diff extends beyond Bicep lines. It compares four sets: resources managed now, resources declared next, shared resources, and resources holding state or data that IaC cannot simply reconstruct.

text managed-resource-review.txt
For every managed resource
Current Resource ID
Declaration present in the new template
Technical owner and data owner
Shared with another application
Inbound and outbound dependencies
Data or configuration not recreated by IaC
Expected action when it leaves the stack
Restore or recreation evidence

Classify the resource
KEEP    remains in the template and stack
DETACH  leaves the stack but remains in Azure
DELETE  explicit, approved and recoverable deletion
BLOCK   ownership, dependencies or recovery unknown

A shared resource should not be deleted because it was placed in the wrong historical module. Conversely, detaching everything by default hides orphaned resources. Classification must be explicit and tied to an owner.

Separate The Diff From Unmanage Behavior

When a resource is no longer managed by the new definition, unmanage behavior determines what happens to it. The control point is therefore the combination of the diff and lifecycle action, not either item in isolation.

yaml unmanage-decision-matrix.yml
removed_from_template:
log_analytics_workspace:
  stateful: true
  shared: true
  decision: detach
  reason: workspace consumed by other workloads
  follow_up: move ownership to observability stack

obsolete_dashboard:
  stateful: false
  shared: false
  decision: delete
  reason: replacement validated and no inbound dependency
  rollback: redeploy previous dashboard module

key_vault:
  stateful: true
  shared: unknown
  decision: block
  reason: secret consumers and recovery path not proven

Do not choose a global action because it fits most resources. If the batch mixes disposable resources and resources with data, split the change or reorganize ownership before updating the stack.

Treat Deny Settings As An Operations Path

A useful deny setting protects the stack against deletion or modification outside the controlled path. A poorly bounded deny also blocks legitimate work: incident response, rotation, recovery, a security pipeline or an approved deletion.

text deny-settings-review.txt
Required questions
Which deny mode is requested, and against which threat?
Does the deny apply to child scopes?
Which identities must still deploy, recover or delete?
Are excluded principals stable and expected object IDs?
Is there an emergency identity with logging and a procedure?
Do Azure Policy, locks and existing deny assignments overlap?

Block promotion
Excluded principal cannot be identified
Exclusion is broad at group or subscription level
Normal pipeline cannot update the stack
Recovery team has no tested path
Child scope protected without an owner inventory

The important test is not whether an administrator can bypass the control. It is whether the intended identity can perform the intended operation while all other identities are rejected as expected.

Test The Scenario, Not Only The Template

Reproduce the update in a representative nonproduction scope. Include a removed resource, a retained resource, a denied attempt and an allowed operation by an excluded identity. Then compare effective resources, not only deployment status.

yaml preproduction-stack-test.yml
test_cases:
- name: managed_resource_stays_managed
  expected: resource_present_and_owned_by_stack
- name: detached_resource_survives_update
  expected: resource_present_without_stack_ownership
- name: approved_delete_removes_only_target
  expected: target_absent_and_neighbors_unchanged
- name: unauthorized_delete_is_denied
  expected: deny_evidence_recorded
- name: pipeline_identity_can_update
  expected: update_succeeds_with_expected_identity
- name: rollback_path_recreates_configuration
  expected: application_probe_returns_to_baseline

evidence:
- before_and_after_resource_ids
- deployment_operation_logs
- identity_object_ids
- deny_result
- application_and_observability_probes

A successful deployment does not prove lifecycle correctness. The test must show that a detached resource still exists, deletion remains bounded and deny protection does not disable operations.

Add A Promotion Gate

The pipeline should refuse an update when it cannot explain removed resources or deny effects. Human approval remains useful, but it should review a concrete artifact.

text deployment-stack-promotion-gate.txt
Allow the update
Effective stack state captured
Current inventory compared with new template
Every removed resource classified DETACH or DELETE
BLOCK resources removed from the change batch
Unmanage action matches every resource lifecycle
Deny settings tested with allowed and denied identities
Application and observability validation ready
Before export and rollback path available

Reject the update
Stateful resource removed without an owner
Global destructive action applied to a mixed batch
Excluded principal added without justification
Child scope affected without an inventory
Rollback limited to redeploying the previous template

The last point matters: redeploying the previous template may recreate a resource, but it does not automatically restore its data, secrets, history or dependencies.

Validate After The Update And Decide Rollback

After promotion, capture the stack again and verify expected Resource IDs. Test application paths, alerts, identity access and deny behavior. An unexplained difference should stop subsequent changes.

text post-update-decision.txt
Keep the update
Every KEEP resource remains managed
Every DETACH resource exists and has a new owner
Only approved DELETE resources disappeared
Deny settings reject and allow the expected identities
Application probes and observability signals are healthy

Correct without global rollback
Metadata, owner or identity exclusion is incomplete
Detached resource is healthy but not yet adopted by a new stack
Protection is too broad and no resource was removed

Roll back or restore
Unapproved resource was deleted
Pipeline or emergency procedure is blocked by deny
Application regression correlates with the change
Stateful resource was recreated without its data

Before rollback
Freeze concurrent writes
Preserve before and after exports
Choose data restore, redeployment or targeted detach
Revalidate the application after each step

Conclusion

An Azure Deployment Stack makes IaC ownership more explicit, but it does not decide which resources are disposable, shared or recoverable. A safe update connects the template diff, effective inventory, unmanage behavior, deny settings and operations identities.

The final decision must be clear: promote when every scope exit and access denial is proven, detach when ownership changes without deletion, split the batch when lifecycles differ, or roll back with a recovery plan when effective state no longer matches the contract.