Removing Bicep Code Does Not Always Remove the Resource? Use Deployment Stacks

Published on:

CloudTrips removes a temporary blob container from Bicep, but a normal incremental deployment leaves the existing container in Azure. The code and the environment now disagree, and nobody clearly owns the leftover resource.

Use an Azure deployment stack for resources whose lifecycle must follow the Bicep definition. A deployment stack is an Azure resource that records the resources deployed through it. When a managed resource disappears from the template, the stack can detach it or delete it.

This lab intentionally deletes a blob container. Use the dedicated resource group below, not an existing CloudTrips environment.

This trip builds on: Use Conditions and Loops. That trip demonstrates why a normal incremental deployment can leave a removed conditional resource behind; this isolated lab shows how a deployment stack changes that lifecycle.

Prepare the Disposable Lab

From the repository root, select the CloudTrips TEST subscription and create the empty lab resource group:

az account set \
  --subscription "<CloudTrips TEST subscription name or ID>"

az group create \
  --name rg-cloudtrips-bicep-stack-test-weu \
  --location westeurope

The cloudtrips-bicep/deployment-stacks/initial.bicep template creates one storage account with two private containers:

resource uploadsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-06-01' = {
  parent: blobService
  name: 'uploads'
  properties: {
    publicAccess: 'None'
  }
}

resource temporaryContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-06-01' = {
  parent: blobService
  name: 'temporary'
  properties: {
    publicAccess: 'None'
  }
}

The companion cloudtrips-bicep/deployment-stacks/without-temporary-container.bicep defines the same storage account, Blob service, and uploads container, but contains no temporary container resource.

Lint both states before using them:

az bicep lint \
  --file cloudtrips-bicep/deployment-stacks/initial.bicep

az bicep lint \
  --file cloudtrips-bicep/deployment-stacks/without-temporary-container.bicep

Create the Deployment Stack

Create a resource-group-scoped stack from the initial template:

az stack group create \
  --name stack-cloudtrips-storage-lifecycle-test \
  --resource-group rg-cloudtrips-bicep-stack-test-weu \
  --template-file cloudtrips-bicep/deployment-stacks/initial.bicep \
  --action-on-unmanage deleteResources \
  --deny-settings-mode none \
  --yes

deleteResources tells the stack to delete a managed Azure resource when that resource is removed from a later template. deny-settings-mode none avoids adding deny assignments; this lab focuses only on lifecycle management.

Inspect the stack and the resource IDs it manages:

az stack group show \
  --name stack-cloudtrips-storage-lifecycle-test \
  --resource-group rg-cloudtrips-bicep-stack-test-weu \
  --query "{state:provisioningState,managedResources:resources[].id}" \
  --output yaml

The state should be succeeded, and the managed resources should include both the uploads and temporary containers. In the Azure portal, open the resource group and select Deployment stacks > stack-cloudtrips-storage-lifecycle-test to see the same managed-resource list.

Successful CloudTrips deployment stack showing the storage account and both managed containers

Prove Why a Normal Deployment Is Not Enough

Preview the template that no longer contains the temporary container as a normal resource-group deployment:

az deployment group what-if \
  --resource-group rg-cloudtrips-bicep-stack-test-weu \
  --template-file cloudtrips-bicep/deployment-stacks/without-temporary-container.bicep

The ordinary incremental preview does not propose deleting temporary. Resources absent from an incremental template are normally left in Azure.

Now update the existing stack with that same template:

az stack group create \
  --name stack-cloudtrips-storage-lifecycle-test \
  --resource-group rg-cloudtrips-bicep-stack-test-weu \
  --template-file cloudtrips-bicep/deployment-stacks/without-temporary-container.bicep \
  --action-on-unmanage deleteResources \
  --deny-settings-mode none \
  --yes

This is an update because the stack name and scope already exist. Azure compares the stack’s previous managed-resource list with the new template. The temporary container is now unmanaged, so deleteResources deletes it. The storage account, Blob service, and uploads container remain managed.

Inspect the result:

az stack group show \
  --name stack-cloudtrips-storage-lifecycle-test \
  --resource-group rg-cloudtrips-bicep-stack-test-weu \
  --query "{state:provisioningState,deletedResources:deletedResources[].id,managedResources:resources[].id}" \
  --output yaml

The state should again be succeeded. The deleted-resource list should contain the temporary container, while the managed-resource list should still contain uploads.

Updated CloudTrips deployment stack showing the temporary container deleted and uploads still managed

Choose the Lifecycle Behavior Deliberately

--action-on-unmanage controls what happens when a resource leaves the template or when the stack itself is deleted:

Value Result
detachAll Keep the resources in Azure but stop managing them with the stack
deleteResources Delete managed resources but not managed resource groups
deleteAll Delete managed resources and any resource groups managed by a broader-scope stack

Use detachAll when ownership should transfer without deleting anything. Use a delete option only when the stack is intended to own the complete lifecycle and the deletion has been reviewed.

Remove the Disposable Lab

Delete the stack and its remaining managed resources:

az stack group delete \
  --name stack-cloudtrips-storage-lifecycle-test \
  --resource-group rg-cloudtrips-bicep-stack-test-weu \
  --action-on-unmanage deleteResources \
  --yes

Then delete the empty lab resource group:

az group delete \
  --name rg-cloudtrips-bicep-stack-test-weu \
  --yes

CloudTrips has now demonstrated the difference between deploying a template and managing a resource lifecycle. Removing a definition from a normal incremental deployment leaves the resource behind; removing it from a stack configured with deleteResources deletes the resource intentionally.