Repeatable Environments: Deployment Stacks and Template Specs
Why this matters
A landing zone (module 02) is only useful if you can hand out many of them consistently. When a new team needs an environment, you don't want someone click-opsing resources or copy-pasting a script — you want a versioned, governed template that provisions the whole thing the same way every time, and a way to manage the resulting set of resources as one lifecycle unit. Azure has native mechanisms for this — Template Specs and Deployment Stacks — and it also had one (Azure Blueprints) that you'll still find in old docs and that is now deprecated. This module makes you accurate about the current tooling: what each native mechanism is, when to use it, and how it relates to the Terraform you already know (track 9) and will scale in module 05.
Concepts
The problem: repeatable, governed, managed-as-a-unit provisioning
Three requirements stack up when you provision environments at scale:
- Repeatable — the same inputs produce the same environment, every time, with no manual steps to forget. (This is the declarative-IaC argument from track 9 module 00, now at environment granularity.)
- Versioned & governed — the template is a first-class, immutable, shareable artifact with versions, so "environment v2" is a known thing and teams can only deploy blessed versions.
- Managed as a unit — the resources a template creates form one lifecycle: you can see them together, update them together, and delete them together, and ideally prevent someone deleting or drifting them out from under you.
Terraform (track 9) gives you 1 and part of 3 (via state and destroy). Azure's
native mechanisms give you 2 and a stronger version of 3. Knowing both — and
that they overlap — is the point; you don't need all of them, you need the
right one for a given org's tooling.
Template Specs: a versioned, shareable ARM/Bicep template as an Azure resource
A Template Spec is an ARM/Bicep template stored as a first-class Azure
resource, with versions. Instead of passing a template.json file around
(email, repo, wiki), you publish it once as a Template Spec; it lives in a
resource group, has RBAC on it (so you control who can deploy it), and every
publish creates an immutable version. Teams then deploy a specific version
of the spec. This solves requirement 2 (versioned, governed, shareable): the
template is a governed artifact, not a loose file. What a Template Spec does
not do by itself is manage the deployed resources as a tracked unit — it's the
template that's versioned, not the deployment. Think of it as "the blessed
blueprint on a shelf, with a version label," analogous to a pinned, published
Terraform module (track 9 module 04) but native to Azure.
Deployment Stacks: manage a set of resources as one governed lifecycle unit
A Deployment Stack is the newer, GA mechanism that addresses requirement 3 head-on. A stack is an Azure resource that represents a collection of resources deployed together and managed as a single unit. Deploy a template (or a Template Spec) as a stack and Azure tracks exactly which resources the stack owns. That unlocks two things a plain deployment can't:
- Whole-unit lifecycle — update the stack and it reconciles the managed resources (adding, updating, and optionally deleting resources that fell out of the template — like a Terraform apply that prunes); delete the stack and it can delete all its managed resources in one action.
- Deny-settings (drift/deletion protection) — a stack can apply
denySettingsthat prevent anyone from deleting or modifying the stack's managed resources outside the stack itself, even users with permissions. This is the "don't let someone hand-delete a landing-zone resource" guardrail Blueprints used to provide via locks.
Stacks can be scoped at resource group, subscription, or management group level — so a platform team can deploy a governed environment across a scope and lock it against tampering. Stacks are the current native answer to "provision a governed environment and keep it governed."
Azure Blueprints is deprecated — know what replaced it
You will find Azure Blueprints in older governance docs and tutorials. Be accurate: Azure Blueprints has been deprecated by Microsoft (it never left preview), with its retirement in July 2026, and Microsoft's guidance is to use Template Specs and Deployment Stacks instead. Blueprints tried to bundle ARM templates + policy assignments + RBAC assignments into one versioned, lockable package applied at a subscription/MG scope — conceptually attractive for landing zones, but it's now a dead end. The replacement mapping:
| Old Blueprints capability | Current mechanism |
|---|---|
| Versioned, shareable template artifact | Template Spec |
| Deploy + manage resources as one locked unit | Deployment Stack (with denySettings) |
| Bundled policy/RBAC assignments at scope | Policy initiatives (module 03) + RBAC at MG (module 01), assigned directly |
So don't design anything new on Blueprints. If you inherit a Blueprints-based environment, the migration path is Template Specs + Deployment Stacks for the provisioning, and native policy/RBAC assignments for the governance the blueprint used to carry. Being able to say this accurately in an interview or design review is the concrete takeaway.
Where Terraform fits (and why this track still teaches both)
If you're all-in on Terraform (module 05), do you need stacks and specs at all?
Often no — Terraform already gives you repeatability (state), pruning (destroy/
plan), and versioned modules (registry pins). Many orgs standardize on Terraform
and never touch Deployment Stacks. But you should know the native mechanisms
because: (a) some orgs are Bicep/ARM shops and stacks are their idiom; (b)
stacks' denySettings deletion-protection is a native control-plane guarantee
Terraform can't fully replicate (Terraform state doesn't stop a portal user
deleting a resource); and (c) the CAF accelerators and Microsoft reference
material assume this vocabulary. The honest framing: Terraform and Deployment
Stacks are overlapping tools for repeatable provisioning — pick one primary
mechanism per org, and know the other exists. Module 05 goes deep on the
Terraform path because it's this curriculum's spine; this module makes sure you
can name and reason about the native alternative.
Command reference
| Command | What it does | Example |
|---|---|---|
az ts create |
Publishes/updates a Template Spec version | see breakdown below |
az ts show |
Shows a Template Spec (and versions) | az ts show --name env-baseline --resource-group rg-specs --version 1.0.0 |
az deployment group create --template-spec <id> |
Deploys a specific Template Spec version (plain deployment) | az deployment group create -g rg-app --template-spec <version-id> |
az stack sub create |
Creates a Deployment Stack at subscription scope | see breakdown below |
az stack group create |
Creates a Deployment Stack at resource-group scope | az stack group create --name app-stack -g rg-app --template-file main.bicep --deny-settings-mode denyDelete --action-on-unmanage deleteAll |
az stack mg create |
Creates a Deployment Stack at management-group scope | az stack mg create --name lz-stack -m mg-corp --location eastus --template-file lz.bicep --deny-settings-mode denyWriteAndDelete --action-on-unmanage detachAll |
az stack sub list |
Lists Deployment Stacks at a scope | az stack sub list -o table |
az stack sub delete |
Deletes a stack (and, per --action-on-unmanage, its resources) |
az stack sub delete --name lz-stack |
Flag breakdown — az ts create --name env-baseline --version 1.0.0 --resource-group rg-specs --location eastus --template-file env.bicep:
--name env-baseline— the Template Spec resource's name (the artifact on the shelf).--version 1.0.0— the immutable version being published; deployers pin to a version, so publishing1.1.0later doesn't change anyone already on1.0.0(the pinning discipline from track 9 module 04).--resource-group rg-specs— where the spec lives; RBAC on this RG controls who can deploy the spec.--template-file env.bicep— the ARM/Bicep template captured as this version.
Flag breakdown — az stack sub create --name lz-corp --location eastus --template-file lz.bicep --deny-settings-mode denyDelete --action-on-unmanage deleteResources --deployment-resource-group-name rg-lz:
--name lz-corp— the stack's name; the handle for the whole managed unit.--location eastus— where the stack resource itself is stored (not necessarily where every deployed resource goes).--template-file lz.bicep— the template (or--template-spec <id>to deploy a governed spec version as a stack — combining both mechanisms).--deny-settings-mode denyDelete— the drift/deletion protection:denyDeleteblocks deletion of managed resources,denyWriteAndDeleteblocks modification too,nonedisables it. This is the native "lock" that replaces Blueprints locks.--action-on-unmanage deleteResources— what happens to managed resources when they leave the stack or the stack is deleted:deleteResources/deleteAllprunes them,detachAllleaves them behind (unmanaged). This is the whole-unit lifecycle control — the "delete the environment in one action" power.--deployment-resource-group-name rg-lz— for a subscription-scope stack, which RG the resources deploy into.
Hands-on exercises
Template Specs and stack metadata are free; the resources a stack/spec
deploys are real and billable, so every exercise deploys something trivial and
cheap (a storage account or a resource group) and cleans up. Reuse your mg-corp
landing-zone MG from module 02 where noted. Bicep is used for the templates (ARM
JSON works identically); if you don't have Bicep, az bicep install first.
(Azure) Author a tiny environment template. Create
env.bicepdescribing a minimal "environment" — one storage account with a required tag:mkdir -p ~/gov-labs/04 && cd ~/gov-labs/04 cat > env.bicep <<'BICEP' param location string = resourceGroup().location param costCenter string resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: 'govstk${uniqueString(resourceGroup().id)}' location: location sku: { name: 'Standard_LRS' } kind: 'StorageV2' tags: { CostCenter: costCenter } } BICEPThis is the repeatable unit you'll publish and deploy governed.
(Azure) Publish it as a versioned Template Spec. Put the template on the shelf as a governed artifact:
az group create -n rg-specs -l eastus az ts create --name env-baseline --version 1.0.0 \ --resource-group rg-specs --location eastus --template-file env.bicep az ts show --name env-baseline -g rg-specs --query "versions" -o jsonExpect a Template Spec
env-baselinewith version1.0.0. This is now a shareable, RBAC-controllable artifact — not a loose file.(Azure) Deploy a specific spec version (plain deployment). Provision an environment from the versioned artifact:
az group create -n rg-env-a -l eastus SPECV=$(az ts show --name env-baseline -g rg-specs --version 1.0.0 --query id -o tsv) az deployment group create -g rg-env-a --template-spec "$SPECV" --parameters costCenter=CC-1001 az resource list -g rg-env-a -o tableExpect a storage account created from spec
1.0.0. Two teams deploying the same version get identical environments — requirement 1 + 2 met.(Azure) Now deploy as a Deployment Stack and see the managed unit. Deploy the same template as a stack so Azure tracks the resources as one unit:
az stack group create --name env-b-stack -g rg-env-a \ --template-file env.bicep --parameters costCenter=CC-1002 \ --deny-settings-mode none --action-on-unmanage deleteAll --yes az stack group show --name env-b-stack -g rg-env-a --query "resources[].id" -o jsonExpect the stack listing the exact resources it manages. Unlike the plain deployment in exercise 3, this set now has a handle (
env-b-stack) you can manage and delete as one.(Azure) Turn on deletion protection (
denySettings) and prove it. Update the stack todenyDeleteand then try to delete a managed resource directly — Azure blocks it:az stack group create --name env-b-stack -g rg-env-a \ --template-file env.bicep --parameters costCenter=CC-1002 \ --deny-settings-mode denyDelete --action-on-unmanage deleteAll --yes SA=$(az stack group show --name env-b-stack -g rg-env-a --query "resources[0].id" -o tsv) az resource delete --ids "$SA" 2>&1 | head # expect a denial from the stack's deny-settingsExpect the direct delete to be rejected — the stack protects its managed resources even from users with delete permission. This is the native control Terraform state alone can't give you, and the replacement for Blueprints locks.
(Azure) Delete the whole environment in one action. The whole-unit lifecycle payoff — remove the stack and, per
--action-on-unmanage, its resources:az stack group delete --name env-b-stack -g rg-env-a --action-on-unmanage deleteAll --yes az resource list -g rg-env-a -o table # the stack-managed storage account is goneExpect the managed resources deleted together. Compare to Terraform
destroy(track 9) — same "tear down the environment with one command" idea, native.(Written) Map Blueprints to its replacements — accuracy check. Without deploying anything, write down: (a) that Azure Blueprints is deprecated (retiring July 2026) and you would not build new on it; (b) for each of the three things Blueprints bundled — versioned template artifact, locked deploy-as-a-unit, bundled policy/RBAC — which current mechanism replaces it (Template Spec; Deployment Stack with
denySettings; policy initiatives from module 03 + RBAC at MG from module 01). This is the interview-ready takeaway.Diagnose and fix: "someone deleted a landing-zone resource we thought was protected." A governance-real scenario. You deployed a stack with
--deny-settings-mode none(the default in exercise 4) believing it was protected, and a resource got hand-deleted. Reproduce and fix:# (Re)create a stack WITHOUT protection — the mistake: az group create -n rg-env-c -l eastus az stack group create --name unprotected-stack -g rg-env-c \ --template-file env.bicep --parameters costCenter=CC-1003 \ --deny-settings-mode none --action-on-unmanage deleteAll --yes SA=$(az stack group show --name unprotected-stack -g rg-env-c --query "resources[0].id" -o tsv) az resource delete --ids "$SA" # SUCCEEDS — no protectionDiagnose: the stack existed, but
deny-settings-modewasnone, so it tracked the resources without protecting them — a stack is not automatically a lock. Confirm withaz stack group show --name unprotected-stack -g rg-env-c --query "denySettings" -o json(modenone). Fix: redeploy the stack with--deny-settings-mode denyDelete(ordenyWriteAndDeletefor full drift protection) so managed resources can't be deleted/modified outside the stack:az stack group create --name unprotected-stack -g rg-env-c \ --template-file env.bicep --parameters costCenter=CC-1003 \ --deny-settings-mode denyDelete --action-on-unmanage deleteAll --yesLesson: deploying as a stack gives you the managed-unit lifecycle; protection is a separate opt-in (
denySettings) you must set. "It's a stack" ≠ "it's protected."(Azure) Clean up everything. Remove stacks, specs, and lab RGs:
az stack group delete --name unprotected-stack -g rg-env-c --action-on-unmanage deleteAll --yes 2>/dev/null; true az group delete -n rg-env-a --yes --no-wait az group delete -n rg-env-c --yes --no-wait az group delete -n rg-specs --yes --no-wait # removes the Template Spec tooConfirm with
az stack group list -g rg-env-a -o table(should error/empty) andaz group list -o table.
Independent challenge
No commands given — build it yourself, drawing on this module, track 9 (declarative
IaC, versioned modules) and module 02 (landing zones). Design a repeatable
"application landing zone" provisioning mechanism using the current native
tooling: author a small Bicep/ARM template that stands up a minimal governed
environment (at least a resource group's worth of resources with required tags),
publish it as a versioned Template Spec so it's a governed artifact teams
deploy by version, and then deploy it as a Deployment Stack with
deletion-protection (denySettings) turned on and an explicit
action-on-unmanage policy, so the environment is both provisioned repeatably
and protected from ad-hoc tampering. Deploy two "environments" from the same
spec version to prove repeatability, then delete one entirely via its stack in a
single action. Finally, write a short paragraph stating precisely why you would
not use Azure Blueprints for this, and mapping each capability you used
(versioned artifact, locked managed unit) to the mechanism that provided it —
and note in one sentence how you'd achieve the same outcome in Terraform (module
05) and what the native denySettings gives you that Terraform state does not.
Clean up all billable resources.
Stuck? One hint
Combine the two mechanisms rather than choosing between them: publish the
template once with az ts create --name <spec> --version 1.0.0, then deploy it
as a stack by passing the spec's version id to the stack
(az stack group create --template-spec <version-id> --deny-settings-mode denyDelete --action-on-unmanage deleteAll). That gives you the governed,
versioned artifact (Template Spec) and the protected, managed-as-a-unit
lifecycle (Deployment Stack) in one flow — which is exactly the pair of things
Blueprints tried to do in a single now-deprecated object. The Terraform
equivalent is a pinned published module + terraform destroy; the thing
denySettings adds that Terraform can't is a control-plane guarantee that even
a portal user with delete rights can't remove the resource out of band.
Common mistakes & troubleshooting
- Designing anything new on Azure Blueprints. It's deprecated (retiring July
- and never left preview. Use Template Specs + Deployment Stacks + native policy/RBAC assignments instead. Recognize Blueprints in old docs; don't build on it.
- Assuming a Deployment Stack automatically protects its resources. The
managed-unit tracking and the protection are separate — protection requires
--deny-settings-mode denyDelete/denyWriteAndDelete. A stack with modenonetracks but does not lock. (Exercise 8.) - Confusing a Template Spec with a Deployment Stack. A Template Spec is a versioned template artifact (the blueprint on the shelf); a Deployment Stack is a managed set of deployed resources (the running environment). Specs version the template; stacks manage the deployment. They compose.
- Forgetting
--action-on-unmanage. When you delete a stack or a resource leaves it, this flag decides whether the resources are deleted or detached (left orphaned). Not setting it deliberately can leave billable resources behind after you thought you'd torn everything down. - Publishing a new Template Spec version and expecting existing deployments to
change. Versions are immutable and deployers pin to one; publishing
1.1.0doesn't touch anything on1.0.0— you must re-deploy the new version (the same pinning behaviour as track 9 module modules). - Reaching for stacks when your org is all-Terraform. Terraform already gives repeatability and pruning; adding stacks on top can create two competing sources of truth. Pick one primary mechanism per environment.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
Show questions
- What is a Template Spec, and which of the three provisioning requirements (repeatable / versioned-governed / managed-as-a-unit) does it primarily satisfy?
- What does a Deployment Stack give you that a plain deployment does not? Name the two headline capabilities.
- What is
denySettingson a stack, and what governance guarantee does it provide that Terraform state cannot? - Is Azure Blueprints something you'd design a new landing-zone mechanism on today? Explain, and give the replacement for each thing Blueprints bundled.
- You deployed resources as a stack but a user still deleted one. What was almost certainly misconfigured?
- What does
--action-on-unmanage deleteAllvsdetachAllcontrol? - When might an org legitimately skip Deployment Stacks entirely?
Show answers
- A Template Spec is an ARM/Bicep template stored as a first-class, RBAC-controlled Azure resource with immutable versions. It primarily satisfies versioned & governed (a shareable, pinned template artifact) — not managed-as-a-unit by itself.
- Whole-unit lifecycle (update reconciles/prunes the managed set; delete can
remove all managed resources in one action) and deletion/drift protection
via
denySettings. A plain deployment does neither — it just creates resources and forgets the grouping. denySettingsprevents deletion (denyDelete) or modification and deletion (denyWriteAndDelete) of a stack's managed resources outside the stack, even by users with permissions — a native control-plane lock. Terraform state can't stop a portal/CLI user from deleting a resource out of band; it only detects the drift afterward.- No — Blueprints is deprecated (retiring July 2026) and never left preview.
Replacements: versioned template artifact → Template Spec; locked
deploy-as-one-unit → Deployment Stack with
denySettings; bundled policy/RBAC assignments → policy initiatives (module 03) + RBAC at MG (module 01) assigned natively. - The stack's
--deny-settings-modewasnone(the default) — it tracked the resources as a unit but did not protect them. Protection is a separate opt-in (denyDelete/denyWriteAndDelete); being a stack isn't the same as being locked. - What happens to the stack's managed resources when they leave the stack or
the stack is deleted:
deleteAlldeletes them (tear the environment down),detachAllleaves them in place as unmanaged resources (orphaned from the stack). - When the org standardizes on Terraform (module 05), which already provides
repeatability (state), pruning (
destroy/plan), and versioned modules — adding stacks would create a second, competing source of truth. They'd still note stacks' nativedenySettingsas the one capability Terraform lacks.
Next
Continue to 05-multi-subscription-terraform-patterns — you've seen the native provisioning mechanisms; now scale the Terraform from track 9 across multiple subscriptions and environments with provider aliasing, module reuse, and per-environment state strategies.