Homeβ€Ί Blogβ€Ί Azure Architecture Series #28 β€” Deployment Stacks and Managed Resources…
Azure Architecture Azure Architecture Series

Azure Architecture Series #28 β€” Deployment Stacks and Managed Resources

Verified against current vendor documentation on 9 September 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

Business Challenge

Corrected 10 September 2026

As first published, this post said the what-if operation was not supported for deployment stacks, citing the known issues page, which says exactly that and still did on the day of writing. It is wrong. A separate Microsoft page dated 12 August 2026 documents what-if for stacks in full, including that it reports resources which would be detached or deleted. The passages that relied on the absence of a preview have been rewritten rather than quietly patched, and the mistake is left described here because "correctly sourced" and "correct" are not the same thing.

Post #21 introduced deployment stacks from one angle: they are the only way a customer can cause a deny assignment to exist. That framed them as a protection mechanism. It is the smaller half of what they are.

The larger half is lifecycle. A deployment stack is a resource that enables you to manage a group of Azure resources as a single, cohesive unit, and submitting a template to it defines the resources that the stack manages. The stack then keeps a list of those resources — which is precisely the thing post #27 spent a whole post describing as Terraform's burden.

The difference is custody. Terraform hands you a state file to store, encrypt, lock, access-control and reconcile. A deployment stack keeps its list inside Azure, as a property of a Resource Manager resource. That removes four problems in one move: no backend to bootstrap, no blob to lock, no plain-text secrets on disk, and no second thing to back up.

What it does not remove is the underlying difficulty, because the difficulty was never the file. It was that a record of what you manage can disagree with what exists. Microsoft documents four ways that happens, and they are the subject of this post:

  • The list itself can go out of sync, and the tool says so.
  • Deleting the resource group underneath a stack bypasses everything the stack was protecting.
  • Moving a managed resource silently strips its protection.
  • And the preview that would catch all three is newer than most of the documentation describing it.
The preview exists, and Microsoft's own pages disagree about that

Post #27 called the plan preview Terraform's strongest argument, and the reason was that a routine-looking apply can contain a delete. A deployment stack update can contain exactly the same delete — that is what actionOnUnmanage is for. Stacks now have a what-if of their own, documented on 12 August 2026, which reports detached and deleted resources before you apply. The known issues page still says the opposite. Where two vendor pages disagree, the newer and more specific one is the one to believe, and the stale entry is worth knowing about because it is the reason people still say stacks cannot be previewed.

Architecture

Diagram: Azure deployment stacks and managed resources, showing that the stack holds the record of what it manages so Azure keeps it rather than you, the actionOnUnmanage matrix of detachAll deleteResources and deleteAll, the difference between a managed and a detached resource, the limits of 800 stacks and 2000 deny assignments per scope, and four documented failure modes including the stack-out-of-sync error, resource group deletion bypassing deny assignments, a move stripping stack protection, and what-if not being supported
Managed, detached, and the three ways something leaves the list. The four boxes at the bottom are all documented behaviour, not speculation.

Managed and detached

A resource is managed when it is in the template and therefore in the stack's list. Querying the stack returns each entry with a status of managed and a denyStatus, and that list is the record — the stack's equivalent of state.

The interface to that list is the template and nothing else. To add a managed resource, add the resource definition to the underlying Bicep files, and then run the update command. To remove one, delete it from the file and update. There is no import, no state rm, no way to edit the list directly — which is a real simplification over Terraform, and also means the template is the only lever you have when the list is wrong.

A resource is detached when the stack doesn't track or manage it but it still exists within Azure. Detaching is the default: stacks detach rather than delete when a resource leaves the stack's management scope. That is the right default — the failure mode of detaching is an orphan, and the failure mode of deleting is data loss — but it means orphans accumulate silently, and nothing in the stack will tell you about a resource it stopped tracking.

actionOnUnmanage: the switch that makes cleanup possible and dangerous

ValueWhat it doesWhen
detachAllDetach the managed resources and resource groups.The default, and correct for anything holding data you have not consciously decided to lose.
deleteResourcesDelete rather than detach for managed resources only; groups survive.Ephemeral environments where the groups are structural and the contents are not.
deleteAllDelete rather than detach for managed resources and resource groups.Whole-environment teardown — and read the warning below before choosing it.

The warning is Microsoft's own and it is worth quoting exactly: when deleting resource groups with the action-on-unmanage switch set to deleteAll, you delete the managed resource groups and all the resources contained within them. Not the managed resources in them — all the resources contained within them. Anything a colleague created by hand inside a group the stack manages is deleted along with it, and nothing in the stack's record ever mentioned it.

There is one place the asymmetry runs the other way, and it is a useful accident: even if you specify the delete-all switch, unmanaged resources within the resource group where the deployment stack is located prevent both the unmanaged resources and the resource group itself from being deleted. The group holding the stack itself is protected by the presence of things the stack does not manage.

One value to avoid entirely: DeleteResourcesAndResourceGroups appears in the PowerShell help, actually detaches rather than deletes, and is being removed in an upcoming update. The documentation says plainly not to use it.

The four ways the record and reality come apart

1. The list goes out of sync. Updating or deleting a stack can return an error saying the stack might not have an accurate list of managed resources, advising you to check the list for additional values and, if uncertain, redeploy the same template and parameters. There is a BypassStackOutOfSyncError switch, and the guidance around it is unusually firm: use it only after thoroughly reviewing the list, and don't use this switch by default. That switch is the one that converts an uncertain list into a delete.

2. Deleting the resource group bypasses the protection entirely. This is the sharpest of the four. A stack created at resource group scope doesn't manage the parent resource group, because the resource group isn't defined in the Bicep file. So the deny assignments protect the resources, and nothing protects the container — delete the group and you delete the stack and its managed resources with it. The documented remedy is the same advice post #21 gave for a different reason: deploy at subscription scope and include the resource group in the Bicep file. Failing that, a lock on any resource in the group makes the delete fail.

3. Moving a managed resource strips its protection. Deny assignments are evaluated at the resource group scope for Move operations, not at the individual resource scope, so a resource can be moved out of its group even when a deny assignment protects that individual resource — and the stack's protections don't transfer with the resource after it's moved. Post #23 established that a move orphans role assignments; this adds that it also quietly launders a resource out of its stack. The interim mitigation is a ReadOnly lock on the resource group, and usefully, deployment stack operations continue to work with the read-only lock in place — reads and deny-delete protection still function, so the lock costs less than it looks like it should.

4. The preview is newer than the documentation saying there is none. The known issues page states that what-if isn't yet supported for deployment stacks. A separate page, dated 12 August 2026, documents exactly that capability in detail — and it is worth using, because it is the only thing that shows a detach or a delete before it happens. The stack version has a different shape from the deployment version: what-if for a stack creates a what-if result resource, a real Azure resource of type Microsoft.Resources/deploymentStacksWhatIfResults that you name, retrieve later and delete. Post #29 covers it properly.

The limits, and what they say about intended scale

Two numbers bound a scope: 800 deployment stacks and 2,000 deny assignments. Reaching both gives 2000 ÷ 800 = 2.5 deny assignments per stack, which is a reasonable sanity check that stacks are meant to be substantial units rather than one per resource.

Four exclusions are worth knowing before designing around stacks:

  • Implicitly created resources are not managed, so they get neither deny assignments nor cleanup. The AKS example from post #21 applies to the lifecycle half too: the cluster is torn down, the things it created for itself are not.
  • Deny assignments do not support tags, so tag-based exemption patterns do not work here.
  • Deny assignments are not supported at management group scope — though they are supported in a management group stack whose deployment is pointed at a subscription, which is the shape post #21 recommended anyway.
  • Stacks cannot delete Key Vault secrets. Removing secrets from a template means running the update or delete in detach mode, or the operation fails.

Two more constraints on reach: a management group-scoped stack can't deploy to another management group — only to its own or a child subscription — and the Microsoft Graph provider doesn't support deployment stacks, which rules out managing Entra objects this way.

Why This Architecture Holds Up

Stack membership is a lifecycle boundary, and Microsoft says so

The guidance for deciding what goes in a stack is the same principle this series keeps arriving at: consider the management lifecycle of those resources, which includes creation, updating, and deletion.

That is the identical criterion post #27 gave for splitting Terraform state, post #23 gave for resource group membership, and post #26 gave for module boundaries. It is not a coincidence. Anything that keeps a record of a group of resources has to answer the same question about what belongs in the group, and the answer is always the things that are created, changed and destroyed together.

The stack sharpens it, though, because of deleteAll. Membership determines what a teardown removes, and the blast radius runs to whole resource groups including resources the stack never managed. Getting membership wrong in Terraform gives you a confusing plan; getting it wrong in a stack with delete flags gives you a deleted resource group.

Where the stack lives decides both protection and survival

Post #21 argued for placing the stack at the parent scope so the protected team cannot edit the stack that protects them. The resource-group deletion issue adds a second, independent reason for exactly the same placement: a resource-group-scoped stack does not manage its own group, so the group remains deletable and takes the stack with it.

Two different problems, one answer — deploy at subscription scope with the resource group declared in the template. When two unrelated failure modes point at the same design, that design is not a preference.

Preview the destructive run, and keep the procedural controls anyway

Terraform's answer to a destructive operation is to show you the plan. Stacks now have the same answer, and it should be the first control: run what-if against the stack before any update whose actionOnUnmanage is a delete, and read what it reports as detached or deleted.

The procedural controls still matter alongside it, because a preview cannot see everything. Know the actionOnUnmanage value before running anything, export the stack and read its resource list when the out-of-sync error appears, and treat BypassStackOutOfSyncError as an incident-level decision rather than a retry flag. What-if predicts; it does not resolve a list the service has already told you it is unsure about.

Key Architecture Decisions

The shape that works

DecisionWhat to doWhy
Stack scope Subscription scope, with the resource group declared in the template It keeps the stack out of reach of the protected team, and it stops the group being deletable underneath the stack.
Stack membership Group by shared lifecycle — created, updated and destroyed together Microsoft's own criterion, and membership decides what a teardown deletes.
actionOnUnmanage Decide it explicitly every run; detachAll for anything holding data deleteAll removes managed resource groups and everything inside them, including resources the stack never managed.
Out-of-sync errors Export the stack, read the resource list, redeploy the same template. Bypass only after that The documented instruction is to review first and never bypass by default.
Protecting against moves A ReadOnly lock on the resource group Deny assignments do not stop a Move, and stack operations keep working under the lock.
Key Vault secrets Remove them in detach mode Stacks cannot delete Key Vault secrets, so a delete-mode run fails on them.
Entra objects Keep them outside the stack The Microsoft Graph provider does not support deployment stacks.
Before any destructive run Run stack what-if, and export the managed resource list as well What-if reports what would be detached or deleted; the export is what you check when the service says the list may be inaccurate.
DeleteResourcesAndResourceGroups Never It detaches rather than deletes despite the name, and it is being removed.

Closing Thought

Reading posts #27 and #28 together makes the real trade visible, and it is not the one the tooling debate usually has. Both Terraform and deployment stacks keep a record of what they manage, because anything that can clean up after itself has to. The question is only who holds the record and what they get in exchange.

Terraform makes you hold it. In return you get a plan, an ecosystem, and one interface across clouds. Stacks let Azure hold it. In return you get no state file to secure, no backend to bootstrap, no lock to operate — and deny assignments, which Terraform has no equivalent of at all.

What neither buys is freedom from the underlying problem. A record of what you manage will eventually disagree with what exists, whether it lives in a blob you own or a Resource Manager resource you do not. Terraform's answer is refresh-only and a plan you are told to read. The stack's answer is a what-if you are told to review, plus an error message telling you the list may be wrong and a switch you are told not to use by default. Both are asking the same thing of you, which is to look before the delete.

Next in this series

#29 takes the preview seriously: what-if, preflight validation and deployment history — what Azure can tell you before a deployment runs, how accurate that prediction actually is, and the specific expressions it cannot evaluate.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent