# Azure Deployment Stacks Deep Dive: Safe IaC Lifecycle, Deny Settings, and Cleanup

**Level:** Advanced · **Time:** 90–120 minutes · **Tooling:** Bicep, Azure CLI, PowerShell 7, Azure portal

Deployment Stacks turn an Infrastructure-as-Code deployment into a managed lifecycle boundary. In this hands-on Azure Workshopz lab, you will build a small subscription-scoped stack, progressively enable deny settings, prove that direct changes are blocked, make an approved change through the stack, detach one resource safely, and verify an ordered cleanup.

> Cost note: the lab creates no compute, public IP, data-plane workload, or application. Its only billable resource is an empty Standard_LRS Storage account, which may incur minimal charges until cleanup.

![Azure Deployment Stacks lifecycle flow](azure-deployment-stacks-cover-1600x900.png)

## What you will prove

- Your Deployment Stack owns the workshop resource group at **subscription scope**.
- A managed identity cannot be deleted directly after `denyDelete` is enabled.
- A direct tag update is denied after `denyWriteAndDelete`, while a stack-driven tag update succeeds.
- A resource removed from desired state can be retained with `detachAll`, then changed directly because it is no longer stack-managed.
- Cleanup removes every lab artifact and the stack-owned deny assignment without modifying the captured starting baseline.

## Architecture

```text
Bicep → subscription-scoped Deployment Stack
          ├── managed resource group
          │    ├── hardened empty Storage account
          │    ├── delete-probe managed identity
          │    └── detachable managed identity
          └── deny assignment
               ├── blocks direct delete/write
               └── permits stack-authorized updates
```

The Bicep template declares the resource group at subscription scope, then deploys its child resources through a resource-group module. This is intentional: Microsoft documents a resource-group deletion limitation for resource-group-scoped stacks. The lab also leaves `deny-settings-apply-to-child-scopes` disabled, so the detached identity is editable after it leaves the stack’s explicit resource inventory.

## Before you start

You need Azure CLI 2.88+, Bicep 0.46+, PowerShell 7.6+, and Owner or Azure Deployment Stack Owner capability at the selected subscription. The preflight stage checks that the required providers are already registered; it never registers providers or changes Azure state.

```powershell
$subscriptionId = '<your-subscription-guid>'
pwsh ./Start-AzureDeploymentStacksWorkshop.ps1 -SubscriptionId $subscriptionId -Stage Preflight
```

The safety gate refuses a pre-existing Workshopz resource group or stack, an unavailable storage name, missing permissions, and unregistered providers. It also captures resources, role assignments, and stacks into an ignored private baseline.

## Stage 1 — Deploy the initial stack

```powershell
pwsh ./Start-AzureDeploymentStacksWorkshop.ps1 -SubscriptionId $subscriptionId -Stage Deploy
```

The initial stack uses no deny setting. It creates an isolated West Europe resource group, a hardened Storage account with public network access and shared-key access disabled, a disposable delete-probe identity, and a later-detachable identity.

![Portal Deployment Stacks overview](screenshots/02-portal-stack-overview.png)

## Stage 2 — Prove `denyDelete`

```powershell
pwsh ./Start-AzureDeploymentStacksWorkshop.ps1 -SubscriptionId $subscriptionId -Stage Protect
```

The stack is updated with `denyDelete`, and the script tries to delete only the disposable managed identity. Azure returns a deny-assignment authorization failure. No pre-existing identity or resource is touched.

![CLI direct delete denied](screenshots/05-cli-delete-denied.png)

## Stage 3 — Prove `denyWriteAndDelete`

```powershell
pwsh ./Start-AzureDeploymentStacksWorkshop.ps1 -SubscriptionId $subscriptionId -Stage Enforce
```

The direct Storage tag update must now fail. However, the previous stack update sets the Storage account’s `Lifecycle=Enforced` tag successfully. That distinction is the point: deny settings protect the desired-state boundary without preventing the stack from applying its own authorized update.

The runner deliberately uses the Storage account update operation with its tags. The generic `az tag update` command uses the separate `Microsoft.Resources/tags/write` operation, so it is not a valid equivalent test for a stack’s managed resource-write deny boundary.

![Stack settings in the portal](screenshots/08-portal-deny-settings.png)

## Stage 4 — Retain and detach one identity

```powershell
pwsh ./Start-AzureDeploymentStacksWorkshop.ps1 -SubscriptionId $subscriptionId -Stage Detach
```

The template stops declaring one identity and the update uses `detachAll`. The identity remains in Azure but disappears from the stack-managed inventory. The script then updates its tag directly to prove the resource is no longer protected by this stack.

This is useful when responsibility needs to move to a different IaC boundary—but it is a lifecycle handoff, not an automatic ownership transfer. Record the new owner before using detach in production.

## Stage 5 — Validate the control plane

```powershell
pwsh ./Start-AzureDeploymentStacksWorkshop.ps1 -SubscriptionId $subscriptionId -Stage Validate
```

The validation stage builds Bicep, runs `az stack sub validate`, checks the managed-resource inventory and resource hardening, and emits a sanitized evidence file with booleans and counts only.

![Sanitized validation evidence](screenshots/09-cli-detach-and-validation.png)

## What can go wrong?

If a direct operation succeeds, do not treat the lab as complete. Confirm the stack’s deny mode and wait briefly for the deny assignment to propagate. If the stack cannot set deny settings, confirm you have Owner or the dedicated stack permission rather than ordinary Contributor. If the detached identity remains blocked, verify that it was removed from Bicep desired state and that child-scope deny was not enabled.

## Production guidance

Deployment Stacks are best for clearly bounded, IaC-owned resources. Use normal Azure RBAC, Policy, network controls, and application authorization alongside them. Start with `denyDelete`, create an exception procedure, and only move to `denyWriteAndDelete` once the stack update path is reliable. Decide deliberately between `deleteAll`, `deleteResources`, and `detachAll` whenever desired state changes.

Remember that deny assignments are control-plane protection. They do not govern data-plane access, and Microsoft documents resource-group deletion and resource-type limitations that you should evaluate before production adoption.

## Cleanup

```powershell
pwsh ./Remove-AzureDeploymentStacksWorkshop.ps1 -SubscriptionId $subscriptionId -ConfirmCleanup
```

The cleanup deletes the detached identity directly, deletes the stack with `deleteAll`, waits for the resource group to disappear, confirms the stack is gone, and compares the original protected baseline. A PASS result means no Workshopz resources, stack, or stack-owned deny assignment remain.

## Download

[Download the complete workshop ZIP](https://136c3e71-702b-495c-b214-d18f0f6b829a.usrfiles.com/archives/32fce1_fe18f472b5224ba4941d1434cccd8375.zip)

## References

- [Azure Deployment Stacks overview](https://learn.microsoft.com/azure/azure-resource-manager/bicep/deployment-stacks)
- [Deployment Stacks known issues](https://learn.microsoft.com/azure/azure-resource-manager/bicep/deployment-stacks-known-issues)
- [Azure CLI Deployment Stacks commands](https://learn.microsoft.com/cli/azure/stack/sub)
