ARM Deployment Stacks now GA!
Published May 23 2024 12:23 PM 3,506 Views
Microsoft

TL; DR– Deployment Stacks is a new resource type for managing a collection of Azure resources as a single unit for faster update and delete (cleanup), while also preventing unwanted changes to those resources. Now Generally Available!

The Problem: 
managing the lifecycle (creates, updates, deletes) of resources across multiple Azure scopes (Resource Group, Management Groups, Subscription) is both complex and time consuming. On top of that, ensuring resources have the proper guard rails in place adds more complexity to the deployment and management of those resources.

 

First let's review common scenarios of where this added complexity is seen today:

  • Cleanup: resources with the same lifecycle are often created across multiple scopes. If you ever need to delete these resources, it requires manually navigating to each scope to clean them up (e.g. deleting test Storage accounts and VMs across numerous resource groups).
  • Unwanted (Accidental) changes: many developers require contributor and owner rights to certain scopes to work on their projects. Sometimes accidental changes can be done to resources (e.g. deleting resources, changing to a more premium SKU) using other clients (e.g. a user updating a "bicep-managed" or "ARM-Template" resource directly with the portal). This makes reconciling difficult when you need to redeploy that bicep file or ARM Template.

Why Deployment Stacks?

Deployment Stacks will enable users to deploy a collection of resources across scopes as a single atomic unit (Bicep or ARM Template). The deployment stack protects its managed resources against unwanted changes. 

The Solution: 1st Party resource enabling 1-to-many CRUD operations and resource change prevention. 

  • Cleanup: easily delete or update resources across scopes with a single update to the deployment stack resource as a 1-to-many operation. You can also delete the entire stack to clean up the entire set of managed resources in one atomic action.
  • Unwanted (Accidental) changes: block changes to managed resources with the deny settings capability of a deployment stack.

The Deployment Stack Resource - Key Concepts

 A deployment stack is a method of deploy an ARM Template or Bicep file which tracks the resources deployed in a "managedResources" list. Beyond the capabilities of conventional ARM Template or Bicep deployments, there are two main capabilities that deployment stacks bring to Azure:

  1. "ActionOnUnamange": With this setting a deployment stack resource knows what action to take when a managed resource becomes unmanaged (removed from the ARM/Bicep template). A "managedResources" can be either deleted or detached. One can trigger the "actionOnUnmanage" behavior directly on all "managedResources" by deleting the deployment stack along with the desired setting or indirectly by removing resources from the template passed into the next deployment stack update along with the desired setting. 
    1. "DeleteResources": this setting will delete resources that become unmanaged. Resource Groups and Management Groups will be detached.
    2. "DeleteAll": this setting will delete resources, resource groups, AND management groups that become unmanaged.
    3. "DetachAll": this setting will detach resources. In other words, the resources are removed from the deployment stack, and will continue to exist in Azure.
  2. "DenySettingsMode": this setting enables a denyAssignment that prevents any changes to "managedResources", attempted from outside of the deployment stack.
    1. "DenyDelete": this setting will enable a denyAssignment that will block all attempted deletes to "managedResources"
    2. "DenyWriteAndDelete": this setting will enable a denyAssignment that will block all attempted writes and deletes to "managedResources".
    3. "None": this setting disables the denyAssignment.

The Deployment Stack Resource - Create and Update

A deployment stack can be created at different scopes, such as, Resource Group, Subscription, and Management Group scope. To create a deployment stack, we need the following information:

  1. A main template, main.bicep or azuredeploy.json, that defines the "managedResources" to be created by the deployment stack. Think of which resources that share the same lifecycle can be defined into a deployment stack (e.g. networking resources, DevTest environments, Applications). For example, here is "mainAppInfra.bicep":
    targetScope = 'subscription'
    
    param resourceGroupName1 string = 'testapp-storage'
    param resourceGroupName2 string = 'testapp-network'
    param resourceGroupLocation string = deployment().location
    
    //Create Resource Groups
    resource testrg1 'Microsoft.Resources/resourceGroups@2021-01-01' = {
      name: resourceGroupName1
      location: resourceGroupLocation
    }
    
    resource testrg2 'Microsoft.Resources/resourceGroups@2021-01-01' = {
      name: resourceGroupName2
      location: resourceGroupLocation
    }
    
    //Create Storage Accounts
    module firstStorage 'multistorage.bicep' = if (resourceGroupName1 == 'testapp-storage') {
      name: uniqueString(resourceGroupName1)
      scope: testrg1
      params: {
        location: resourceGroupLocation
      }
    }
    
    //Create Virtual Networks
    module firstVnet 'multinetwork.bicep' = if (resourceGroupName2 == 'testapp-network') {
      name: uniqueString(resourceGroupName2)
      scope: testrg2
      params: {
        location: resourceGroupLocation
      }
    }​

    This file deploys storage account and virtual network to different resource groups.

  2. Choose "ActionOnUnmanage" setting of "DeleteResources", "DeleteAll" or "DetachAll".
  3. Choose "DenySettingsMode" setting of "DenyDelete", "DenyWriteAndDelete" or "None". 
  4. Choose the scope of the deployment stack and target scope of its deployment.

To help us start visualizing this, let's look at what an Azure CLI command to create deployment stack at subscription scope looks like:

 

 

 

 

 

 

 

 

az stack sub create --name "DevTestEnvStack" --template-file "mainAppInfra.bicep" --location "westus2" --action-on-unmanage "deleteResources" --deny-settings-mode "denyDelete"

 

 

 

 

 

 

 

 

Here is the response from that command (some stack properties removed for simplicity of example):

 

 

 

 

 

 

 

{
  "actionOnUnmanage": {
    "managementGroups": "detach",
    "resourceGroups": "detach",
    "resources": "delete"
  },
  "deletedResources": [],
  "denySettings": {
    "applyToChildScopes": false,
    "excludedActions": [],
    "excludedPrincipals": null,
    "mode": "denyDelete"
  },
  "deploymentId": "/subscriptions/***/providers/Microsoft.Resources/deployments/DevTestEnvStack-24052002bd5h1",
  "detachedResources": [],
  "failedResources": [],
  "id": "/subscriptions/***/providers/Microsoft.Resources/deploymentStacks/DevTestEnvStack",
  "location": "westus2",
  "name": "DevTestEnvStack",
  "provisioningState": "succeeded",
  "resources": [
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-network",
      "status": "managed"
    },
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-network/providers/Microsoft.Network/virtualNetworks/testnetbjildrqs4q6ve",
      "resourceGroup": "testapp-network",
      "status": "managed"
    },
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-storage",
      "status": "managed"
    },
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-storage/providers/Microsoft.Storage/storageAccounts/teststore1ic7t5vnieyika",
      "resourceGroup": "testapp-storage",
      "status": "managed"
    },
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-storage/providers/Microsoft.Storage/storageAccounts/teststore2ic7t5vnieyika",
      "resourceGroup": "testapp-storage",
      "status": "managed"
    }
  ],
  "type": "Microsoft.Resources/deploymentStacks"
}

 

 

 

 

 

 

In this example, a deployment stack named "DevTestEnvStack" was created, and the resulting output of the command shows the details about the deployment stack resource and its managed resources. Note the "status" of each resource as "managed". To refer back to those details, you can use the show command in CLI:

 

 

 

 

 

az sub stack show --name "DevTestEnvStack"

 

 

 

 

 

 

The result will contain all information on the specified deployment stacks object, such as, resource ID of the deployment stack, array of managed resources, deny setting configurations, and actionOnUnmanage settings. Let's take a look at "actionOnUnmanage" in particular:

 

 

 

 

 

"actionOnUnmanage": {
    "managementGroups": "detach",
    "resourceGroups": "detach",
    "resources": "delete"
}

 

 

 

 

 

Given the current configuration, if we were to remove a resource from the mainAppInfra.bicep template, that resource will be deleted by the Deployment Stack. In our example, let's remove the virtual network resource from our template:

 

 

 

targetScope = 'subscription'

param resourceGroupName1 string = 'testapp-storage'
param resourceGroupName2 string = 'testapp-network'
param resourceGroupLocation string = deployment().location

//Create Resource Groups
resource testrg1 'Microsoft.Resources/resourceGroups@2021-01-01' = {
  name: resourceGroupName1
  location: resourceGroupLocation
}

resource testrg2 'Microsoft.Resources/resourceGroups@2021-01-01' = {
  name: resourceGroupName2
  location: resourceGroupLocation
}

//Create Storage Accounts
module firstStorage 'multistorage.bicep' = if (resourceGroupName1 == 'testapp-storage') {
  name: uniqueString(resourceGroupName1)
  scope: testrg1
  params: {
    location: resourceGroupLocation
  }
}

 

 

 

 

 

Now let's we redeploy the deployment stack with the same command:

 

 

 

 

az stack sub create --name "DevTestEnvStack" --template-file "mainAppInfra.bicep" --location "westus2" --action-on-unmanage "deleteResources" --deny-settings-mode "denyDelete"

 

 

 

Here is the response from that command (some stack properties removed for simplicity of example):

 

 

 

{
  "actionOnUnmanage": {
    "managementGroups": "detach",
    "resourceGroups": "detach",
    "resources": "delete"
  },
  "deletedResources": [
    {
      "id": "/subscriptions/***/resourceGroups/testapp-network/providers/Microsoft.Network/virtualNetworks/testnet1bjildrqs4q6ve",
      "resourceGroup": "testapp-network"
    },
    {
      "id": "/subscriptions/***/resourceGroups/testapp-network/providers/Microsoft.Network/virtualNetworks/testnet2bjildrqs4q6ve",
      "resourceGroup": "testapp-network"
    }
  ],
  "denySettings": {
    "applyToChildScopes": false,
    "excludedActions": [],
    "excludedPrincipals": null,
    "mode": "denyDelete"
  },
  "deploymentId": "/subscriptions/***/providers/Microsoft.Resources/deployments/DevTestEnvStack-24052317bd010",
  "deploymentScope": null,
  "description": null,
  "detachedResources": [],
  "failedResources": [],
  "id": "/subscriptions/***/providers/Microsoft.Resources/deploymentStacks/DevTestEnvStack",
  "location": "westus2",
  "name": "DevTestEnvStack",
  "provisioningState": "succeeded",
  "resources": [
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-network",
      "status": "managed"
    },
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-storage",
      "status": "managed"
    },
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-storage/providers/Microsoft.Storage/storageAccounts/teststore1ic7t5vnieyika",
      "resourceGroup": "testapp-storage",
      "status": "managed"
    },
    {
      "denyStatus": "denyDelete",
      "id": "/subscriptions/***/resourceGroups/testapp-storage/providers/Microsoft.Storage/storageAccounts/teststore2ic7t5vnieyika",
      "resourceGroup": "testapp-storage",
      "status": "managed"
    }
  ],
  "type": "Microsoft.Resources/deploymentStacks"
}

 

 

 

 

 

 

Note that the virtual network resource is no longer "managed" and can now be seen in the "deletedResources" array property of the deployment stack response. This shows how deployment stacks can be used to easily delete resources by removing them from the template with the appropriate "--actionOnUnmanage" behavior defined.

 

You can also view the Deployment Stack and its contents in portal by navigating to the specified scope (Subscription for this example) > settings > deployment stacks. 

azcloudfarmer_0-1716177997369.png

Select the deployment stack "DevTestEnvStack" to view:

azcloudfarmer_1-1716178574109.png

Beyond deciding on the behavior for "actionOnUnmanage" it is also important to define what deny settings mode should the deployment stack use. This enables guard-rails to help protect your "managedResources" against unwanted changes. In our initial example, we specified DenyDelete for our deny settings mode. Behind the scenes, our deployment stack resource created a deny assignment for each of its managedResources. This means that other users (not our deployment stack) can make updates/writes to the provisions test storage accounts, but can't delete them, even if they have owner access to that resource and scope.

 

In some cases, you might need some flexibility or stop gap measure for the deny settings mode. For example, you may want to exclude a particular admin user from the deny assignment, such that they can go and delete the resource manually (outside of the context of a deployment stack), or maybe you want to exclude specific actions for all users (e.g. Still allow all users to perform Writes and Deletes to storage accounts and virtual network resource types). This can be done with the following exclusion parameters for deny settings:

  1. "DenySettingsExcludedPrincipals": this setting will exclude the list of AAD Principal IDs from the denyAssignment.
  2. "DenySettingsExcludedActions": this setting will exclude the list of RBAC Actions from the denyAssignment.

In our example, if we were to decide to exclude a specific user ID and also exclude the ability to delete storage accounts from the deployment stack's deny settings, the command will now look like this:

 

 

 

 

 

 

 

 

az stack sub create --name "DevTestEnvStack" --location "westus2" --template-file "mainAppInfra.bicep" --actionOnUnmanage "DeleteAll" --deny-settings-mode "DenyDelete" --deny-settings-excluded-principals "12304812408-2148124081" --deny-settings-excluded-actions "Microsoft.Storage/storageAccounts/delete"

 

 

 

 

 

 

 

 

These exclusion flags give you the flexibility to enable access to "managedResources" for specific users or specific actions, while keeping all other "managedResources" secured with deny settings. For more information on deployment stacks, please visit our docs and our GitHub.  

 

Deployment Stacks Docs Reference:

Co-Authors
Version history
Last update:
‎May 23 2024 11:47 AM
Updated by: