Using Azure deployment stacks to protect what you deployed

5 min readMarkdown version

Short answer

Deploy them through an Azure deployment stack with deny settings. A stack manages a set of resources as one unit and creates a deny assignment over them, with a mode of none, denyDelete or denyWriteAndDelete, which applies to everyone except the principals you explicitly exclude. It covers control plane operations only, not data plane ones, and only resources the template creates explicitly, so it stops a subscription owner deleting a host pool but not someone writing into a storage container.

The problem stacks solve for an MSP

You deploy an AVD environment into a customer's subscription. The customer, or their previous IT provider, or an enthusiastic internal admin, has Owner on that subscription. Nothing in a normal ARM deployment stops one of them deleting the host pool, resizing the hosts or removing a network interface, and the first you hear about it is a ticket.

Microsoft describes a deployment stack as a resource that lets you manage a group of Azure resources as a single, cohesive unit, and lists prevention of undesired modifications to managed resources via deny settings among its benefits. That is the property that matters here.

Deny settings, and their three modes

The deny settings mode defines the operations prohibited on the managed resources, and the restriction applies to everyone unless you explicitly grant access.

  • none: The stack manages lifecycle but blocks nothing.
  • denyDelete: Managed resources can be changed but not deleted. A reasonable default when the customer's own team still operates the environment day to day.
  • denyWriteAndDelete: Managed resources cannot be modified or deleted outside the stack. Strongest, and the one that generates support calls if you apply it without telling anyone.

What deny settings do not cover

Two limits decide whether this is protection or a false sense of it, and both are documented.

First, the deny setting applies only to control plane operations, not data plane operations. Creating a storage account is control plane and is protected. Writing a blob into it, or a secret into a key vault, is data plane and is not.

Second, it applies only to explicitly created resources, not implicitly created ones. Microsoft's example is a managed AKS cluster, which creates supporting resources such as virtual machines that are not defined in the template and are therefore not subject to the stack's deny settings.

Lifecycle: detach or delete

The other half of a stack is what happens when a resource leaves the template. The actionOnUnmanage setting takes detachAll, deleteResources or deleteAll. The default is to detach, meaning the resource keeps existing but the stack stops tracking it.

Which one is safe depends entirely on what the stack is laid over. For a stack covering resources you did not create, detach is the conservative choice. For a stack that owns a resource group it created itself, delete is what keeps a teardown from leaving billable orphans behind. deleteAll on a resource group scope deletes the managed resource groups and everything inside them, so the question to answer before choosing it is whether anything else lives in those groups.

Where to put the stack

Microsoft recommends storing stacks at the parent scope: to protect resources in a subscription, place the stack at the immediate parent management group. Where the stack exists is where the deny assignment is created, so putting the stack a level above the resources it protects means the people working with those resources cannot simply edit the stack to remove their own restrictions.

There are two built-in roles worth knowing: Azure Deployment Stack Contributor can manage stacks but cannot create or delete deny assignments within them, and Azure Deployment Stack Owner can do both.

How rugged.sh uses this

Every template rugged.sh deploys goes out wrapped in a deployment stack with denySettings set to denyWriteAndDelete, which gives both halves of the problem in one primitive: a clean lifecycle handle for updating or removing an environment, and a deny assignment protecting what we deployed from being changed out from under us. The documented limits above still apply, so it pairs with drift detection, which is what catches the changes a deny assignment deliberately does not cover.

On the lifecycle half we deploy with actionOnUnmanage set to delete for both resources and resource groups, not the detach default recommended above for stacks laid over an estate you did not build. The reason is the scoping: each deployment gets its own resource group that rugged.sh creates for it and nothing else, so removing the deployment removes exactly what that deployment made. Detaching instead would leave orphaned session hosts and disks billing against the customer subscription with nothing tracking them, which is the failure mode we think is worse. If you author your own stacks over resources you did not create, the detach default above is still the right starting point.

Questions people ask

What is an Azure deployment stack?
A resource type, Microsoft.Resources/deploymentStacks, that manages a group of Azure resources defined by a Bicep file or ARM template as a single unit. It can perform updates across scopes to the resources it describes and block unwanted changes to them through deny settings.
What are the deny settings modes?
none, denyDelete and denyWriteAndDelete. The mode defines which operations are prohibited on managed resources, and the restriction applies to everyone unless a principal is explicitly excluded, up to a maximum of five excluded principals.
Do deny settings stop someone reading or writing data in a protected resource?
No. Deny settings apply to control plane operations only. A key vault created by the stack is protected from deletion, but secrets inside it are data plane objects and are governed by that resource's own access model.
What happens to resources when I delete a deployment stack?
It depends on actionOnUnmanage: detachAll leaves them in place but untracked, deleteResources removes the resources, and deleteAll removes the resources and their resource groups. Detach is the default behaviour for resources that fall out of a stack's scope.

Sources

Microsoft's behaviour described above was read from these pages on the dates shown. Claims that come from our own testing against a live Azure subscription carry that date inline.

Related

Try it on one client

The free tier covers one customer tenant and five users, with no time limit, against your own Azure subscription. Paid plans are priced per tenant, not per user.