Azure Blueprints Retirement: What Breaks on October 31 and How to Migrate to Deployment Stacks

Azure Blueprints Retirement: What Breaks on October 31 and How to Migrate to Deployment Stacks

If a subscription-vending pipeline in your tenant still runs az blueprint assignment create or New-AzBlueprintAssignment to stamp policies, role assignments and a locked resource group onto every new subscription, it has twelve days left. On October 31, 2026 Azure Blueprints stops accepting new assignments and freezes existing definitions. That is phase two of a retirement that ends on January 31, 2027, when the service stops responding, unexported definitions are deleted, and every blueprint lock disappears from the resources it was protecting.

That last part is the one that bites quietly. Blueprint locks are deny assignments, and they stop even a subscription Owner from deleting or changing a resource. When they vanish, nothing errors. Your effective RBAC just gets wider overnight.

This guide covers what changes on each date, how to find where Blueprints is still in use, how to export and triage what you have, and how to rebuild it as a template spec deployed through a deployment stack, in an order that never leaves the resources unprotected.

The Timeline, Date by Date

Microsoft first announced the retirement in September 2023 with a July 11, 2026 end date. That was later pushed back and split into phases. From the official retirement page:

DateWhat changes
July 31, 2026New blueprint definitions and versions can no longer be created
October 31, 2026Existing definitions can no longer be modified. New assignments can no longer be created
December 31, 2026Existing assignments can no longer be modified
January 31, 2027Service retired: API stops responding, CLI and PowerShell commands stop working, portal blade removed, unexported definitions/versions/assignments deleted, blueprint locks stop functioning. Deployed resources stay

Two more details are easy to miss:

  • The Azure CLI drops the command group early. The az blueprint reference page says the group is deprecated and will be removed in the next breaking-change release, 2.92.0, scheduled for November 2026. If your export runs on a build agent that upgrades the CLI automatically, run it before that release lands, or pin the CLI version on that agent.
  • December 31 is the real deadline for lock changes. Changing an assignment's lock mode is an assignment update. After December 31 you can only read or delete assignments. That matters for the migration order later in this guide.

Step 1: Find Where Blueprints Is Still Used

Microsoft names two official ways to find usage: an Azure Advisor recommendation that lists subscriptions and management groups using Blueprints, and the Blueprints blade in the portal. To get the same inventory from a script, use the CLI extension while it still exists (it installs itself the first time you run an az blueprint command):

1# Definitions stored at a management group, and at each subscription
2az blueprint list --management-group "<mg-id>" -o table
3
4for sub in $(az account list --query "[].id" -o tsv); do
5  echo "== $sub"
6  az blueprint list --subscription "$sub" --query "[].name" -o tsv
7  az blueprint assignment list --subscription "$sub" \
8    --query "[].{name:name, lock:locks.mode, state:provisioningState}" -o table
9done

Save the locks.mode column. Any assignment showing AllResourcesReadOnly or AllResourcesDoNotDelete is protecting resources with a deny assignment, and those resources lose that protection on January 31. You can see those deny assignments in the portal under Access control (IAM) > Deny assignments on the resource group.

Then search your pipelines for the calls that break on October 31:

1grep -rnE "az blueprint assignment create|New-AzBlueprintAssignment|Microsoft.Blueprint/blueprintAssignments" \
2  --include=*.yml --include=*.yaml --include=*.ps1 --include=*.sh --include=*.json .

A hit in a subscription-vending or landing-zone pipeline is your most urgent item. A pipeline that only reads or deletes keeps working until January.

Step 2: Export Everything You Want to Keep

Definitions you don't export are permanently deleted on January 31, 2027. Export now, even if you are not sure you will reuse them. A few KB of JSON in Git costs nothing.

With the Azure CLI:

1az blueprint export \
2  --management-group "<mg-id>" \
3  --name "CorpLandingZone" \
4  --output-path "./blueprints/CorpLandingZone" \
5  --yes

With PowerShell (the Az.Blueprint module):

1$bp = Get-AzBlueprint -ManagementGroupId "<mg-id>" -Name "CorpLandingZone" -Version "1.4"
2Export-AzBlueprintWithArtifact -Blueprint $bp -OutputPath "./blueprints"

Both produce the same layout: a blueprint.json plus one JSON file per artifact under artifacts/.

The export covers the definition, not the assignments. The parameter values, resource group names and lock settings each assignment was deployed with live on the assignment. Save those too, because you will need the real values to fill in your new template:

1az blueprint assignment show --subscription "<sub-id>" --name "<assignment-name>" \
2  -o json > "./blueprints/assignments/<sub-id>-<assignment-name>.json"

Step 3: Triage the Export

Every exported artifact is one of three kinds: template, policyAssignment or roleAssignment. Each maps to a plain ARM resource. The work is in the blueprint-level expressions inside the artifact wrappers. An artifact that passes [parameters('owners')] is reading a blueprint parameter that will not exist once the blueprint is gone, and artifacts('network').outputs.vnetId reads another artifact's output, which you will replace with a Bicep module output.

This script lists what you have, flags the expressions you need to rewrite, and pulls each template artifact out as a standalone ARM template. It ignores the embedded template body, where parameters() calls are normal ARM:

 1#!/usr/bin/env bash
 2# triage.sh <exported-blueprint-dir>   (needs jq)
 3set -euo pipefail
 4dir="$1"
 5echo "== Blueprint: $(jq -r '.name // "?"' "$dir/blueprint.json")  targetScope=$(jq -r '.properties.targetScope' "$dir/blueprint.json")"
 6echo "== Blueprint-level parameters (become stack/template params):"
 7jq -r '.properties.parameters // {} | keys[] | "   - " + .' "$dir/blueprint.json"
 8echo "== Resource group placeholders (become Microsoft.Resources/resourceGroups):"
 9jq -r '.properties.resourceGroups // {} | keys[] | "   - " + .' "$dir/blueprint.json"
10echo "== Artifacts:"
11for f in "$dir"/artifacts/*.json; do
12  jq -r --arg f "$(basename "$f")" '"   " + $f + "  kind=" + .kind + "  rg=" + (.properties.resourceGroup // "(subscription)")' "$f"
13done
14echo "== Blueprint-only expressions that need rewriting:"
15found=0
16for f in "$dir"/artifacts/*.json; do
17  # skip the embedded ARM template body: its own parameters() calls are normal ARM
18  hits=$(jq 'del(.properties.template)' "$f" | grep -oE "(parameters?|artifacts|resourceGroups)\('[^']*'\)" | sort -u || true)
19  while IFS= read -r h; do
20    [ -n "$h" ] && { echo "   $(basename "$f"): $h"; found=1; }
21  done <<< "$hits"
22done
23[ "$found" = 1 ] || echo "   none"
24mkdir -p "$dir/../arm"
25for f in "$dir"/artifacts/*.json; do
26  if [ "$(jq -r .kind "$f")" = "template" ]; then
27    out="$dir/../arm/$(basename "$f" .json).template.json"
28    jq '.properties.template + {"$schema":"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"}' "$f" > "$out"
29    echo "== Extracted ARM template -> arm/$(basename "$out")"
30  fi
31done

I ran this against an export rebuilt from the sample artifacts in Microsoft's Blueprints REST API reference (one storage template, one cost-center tag policy, one Owner role assignment). The output:

 1== Blueprint: MyBlueprint  targetScope=subscription
 2== Blueprint-level parameters (become stack/template params):
 3   - costCenter
 4   - owners
 5   - storageAccountType
 6== Resource group placeholders (become Microsoft.Resources/resourceGroups):
 7   - storageRG
 8== Artifacts:
 9   costCenterPolicy.json  kind=policyAssignment  rg=(subscription)
10   ownerAssignment.json  kind=roleAssignment  rg=(subscription)
11   storageTemplate.json  kind=template  rg=storageRG
12== Blueprint-only expressions that need rewriting:
13   costCenterPolicy.json: parameter('costCenter')
14   ownerAssignment.json: parameters('owners')
15   storageTemplate.json: parameters('storageAccountType')
16== Extracted ARM template -> arm/storageTemplate.template.json

Two things are worth noticing. First, Microsoft's own sample policy artifact uses parameter(...) (singular), which is why the pattern matches both spellings. Second, the script is a checklist, not a converter. It tells you what to rewrite. It does not rewrite anything.

To turn an extracted ARM template into Bicep, az bicep decompile --file arm/storageTemplate.template.json gives you a starting point. Expect to tidy up its output by hand.

Step 4: Rebuild It as One Bicep File

Microsoft's mapping is direct: policy assignment artifact to Microsoft.Authorization/policyAssignments, role assignment artifact to Microsoft.Authorization/roleAssignments, template artifact to a module, resource group placeholder to Microsoft.Resources/resourceGroups. Blueprint parameters become Bicep parameters. Set targetScope to match where the blueprint was assigned. For the sample blueprint above, that looks like this:

 1targetScope = 'subscription'
 2
 3param location string = 'westeurope'
 4param costCenter string
 5param ownerPrincipalIds array
 6@allowed([ 'Standard_LRS', 'Standard_GRS', 'Standard_ZRS', 'Premium_LRS' ])
 7param storageAccountType string = 'Standard_LRS'
 8
 9// was: resource group placeholder "storageRG"
10resource storageRg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
11  name: 'rg-storage'
12  location: location
13}
14
15// was: template artifact "storageTemplate"
16module storage 'modules/storage.bicep' = {
17  name: 'storage'
18  scope: storageRg
19  params: {
20    storageAccountType: storageAccountType
21  }
22}
23
24// was: policyAssignment artifact "costCenterPolicy" (definition ID copied as-is from the exported artifact)
25resource costCenterTag 'Microsoft.Authorization/policyAssignments@2024-04-01' = {
26  name: 'require-costcenter-tag'
27  properties: {
28    displayName: 'force costCenter tag on all resources'
29    policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/1e30110a-5ceb-460c-a204-c1c3969c6d62'
30    parameters: {
31      tagName: { value: 'costCenter' }
32      tagValue: { value: costCenter }
33    }
34  }
35}
36
37// was: roleAssignment artifact "ownerAssignment" (built-in Owner role)
38resource owners 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for principalId in ownerPrincipalIds: {
39  name: guid(subscription().id, principalId, 'owner')
40  properties: {
41    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')
42    principalId: principalId
43  }
44}]

Watch the role definition ID. Microsoft's REST sample artifact uses acdd72a7-3385-48ef-bd42-f606fba81ae7, which is the built-in Reader role, even though its display name says "enforce owners". Owner is 8e3af657-a8ff-443c-a75c-2fe8c4bcb635. Copy the GUID from your exported artifact, not from a sample, and check which role it really is with az role definition list --name <guid> --query "[].roleName".

Before you deploy anything, preview it against the live subscription:

1az deployment sub what-if --location westeurope --template-file main.bicep \
2  --parameters costCenter=CC-1042 ownerPrincipalIds='["<object-id>"]'

If what-if wants to create a policy assignment or role assignment that already exists from the blueprint, your Bicep is using a different name for it. Look up the existing names (az policy assignment list --query "[].{name:name, display:displayName}" and az role assignment list --assignee <object-id> --query "[].name") and reuse them. Otherwise you end up with duplicate policy assignments, or the deployment fails because a role assignment for that principal, role and scope already exists under another name.

Step 5: Publish a Template Spec, Deploy a Stack

A template spec replaces the "stored, versioned definition" half of Blueprints. A deployment stack replaces the "assignment plus locks" half. Publish first:

1az ts create \
2  --name corp-landing-zone \
3  --version 1.0.0 \
4  --resource-group rg-platform-templates \
5  --location westeurope \
6  --template-file ./main.bicep

Then deploy it through a subscription-scope stack:

1az stack sub create \
2  --name corp-landing-zone \
3  --location westeurope \
4  --template-spec "/subscriptions/<sub-id>/resourceGroups/rg-platform-templates/providers/Microsoft.Resources/templateSpecs/corp-landing-zone/versions/1.0.0" \
5  --parameters costCenter=CC-1042 ownerPrincipalIds='["<object-id>"]' \
6  --deny-settings-mode denyDelete \
7  --deny-settings-excluded-principals "<platform-team-group-object-id>" \
8  --action-on-unmanage detachAll

The deny modes map onto blueprint locks: denyDelete is like Do Not Delete, and denyWriteAndDelete is like Read Only.

Note --action-on-unmanage detachAll. Microsoft's migration page uses deleteResources in its example. With that setting, removing a resource from the template on a later update deletes the resource. While you are still iterating on a template that adopted live resources, a typo that drops a resource block should not be able to delete production. Start with detachAll, and switch to a delete mode only after the template has been stable for a few cycles.

Check the result. Every resource you care about should show status: managed and a denyStatus other than none:

1az stack sub show --name corp-landing-zone \
2  --query "resources[].{id:id, status:status, deny:denyStatus}" -o table

Step 6: Hand Over the Locks Without a Gap

The order matters here. It is also where I am reasoning from documented behavior rather than from a test run, so rehearse it in a non-production subscription first.

A blueprint Read Only lock is a deny assignment that blocks every action except reads, for everyone except the blueprint's own managed identity and up to five excluded principals. Your stack deployment runs as you, not as the blueprint identity. So if the stack has to write to resources that a Read Only lock protects, expect those writes to be denied.

The sequence I would use:

  1. Downgrade the blueprint lock from Read Only to Do Not Delete. This allows writes but keeps delete protection. It must happen before December 31, because it counts as an assignment update:
    1az blueprint assignment update --subscription "<sub-id>" --name "<assignment-name>" \
    2  --locks-mode AllResourcesDoNotDelete
    
  2. Wait. The Blueprints docs warn that Azure Resource Manager caches role assignment details for up to 30 minutes, so lock changes are not instant.
  3. Create the stack with denyDelete (or denyWriteAndDelete if you had Read Only before), as in Step 5. Both deny assignments now overlap, so the resources are never unprotected.
  4. Verify with az stack sub show that every resource has a deny status.
  5. Remove the blueprint assignment and make sure it leaves the resources in place:
    1az blueprint assignment delete --subscription "<sub-id>" --name "<assignment-name>" \
    2  --delete-behavior none --yes
    
    --delete-behavior all deletes the resources the assignment created. Never let that flag get into a migration script.

Assignments made at management group scope were often set up that way so subscription owners could not remove them. Keep that property: Microsoft's stack docs recommend storing the stack at the parent scope of the resources it protects, so place it at the management group with az stack mg create ... --deployment-subscription <sub-id>.

Best Practices

  • Keep the Bicep in Git, and use template specs only where you need sharing inside Azure. Microsoft lists a Git repository as an equally valid replacement for definition storage. If your platform team already reviews infrastructure changes through pull requests, stacks can deploy from the pipeline directly.
  • Exclude groups, not people. Stack deny settings accept at most five excluded principals. The docs say a longer list does not return an error, so you would not find out until the wrong person is denied. Put your break-glass admins, CI/CD identity and policy-remediation identity in one or two Entra groups and exclude those.
  • Give the right people the right role. Only Azure Deployment Stack Owner can create or change deny settings. Azure Deployment Stack Contributor can manage stacks but cannot touch deny assignments. Your vending pipeline's identity needs Owner on the stack scope.
  • Fix the vending pipeline first. It is the only thing that actually stops working on October 31. Everything else has until December or January.

Common Mistakes to Avoid

  • Assuming the locks carry over. They don't. Resources stay on January 31, but their blueprint deny assignments are removed automatically, and nothing tells you.
  • Expecting a stack's deny settings to cover everything. They apply only to control-plane operations, and only to resources explicitly declared in the template. Blob containers, Key Vault secrets and resources created implicitly (for example the VMs behind an AKS cluster) are not covered.
  • Copying Microsoft's sample flags into production. --action-on-unmanage deleteResources and --delete-behavior all both delete real resources in the wrong situation.
  • Exporting only the definition. Assignment parameter values (CIDRs, cost centers, owner object IDs) live on the assignment. Save them in Step 2 or you will be rebuilding them by hand.
  • Letting a build agent upgrade to Azure CLI 2.92.0 before you export. The az blueprint commands are scheduled to be removed in that release.

Troubleshooting

SymptomLikely causeFix
Vending pipeline fails creating an assignment after Oct 31Phase 2: new assignments are disabledReplace the step with az stack sub create / az stack mg create against your template spec
Stack deployment is denied writing to an existing resourceA blueprint Read Only lock still appliesDowngrade the assignment to AllResourcesDoNotDelete, wait up to 30 minutes, retry
Deployment fails because a role assignment already existsSame principal, role and scope already assigned by the blueprint under a different nameReuse the existing assignment's name in Bicep, or remove the blueprint-created one first
Duplicate policy assignments appear after the stack deploysBicep name differs from the blueprint-created assignmentMatch the existing name, then redeploy
An admin is unexpectedly denied by the stackMore than five excluded principals listed (silently accepted)Move exclusions into Entra groups
Stack update refuses with a "stack-out-of-sync" errorThe stack's managed-resource list may be inaccurateRedeploy with the same template and parameters. Use --bypass-stack-out-of-sync-error only after reviewing the resource list

FAQ

Do my resources get deleted on January 31, 2027? No. Resources deployed by blueprints remain. What goes away is the blueprint definitions, assignments and locks. The lock removal is the part that needs action.

Can I still read and export blueprints after October 31? Yes. Read and delete stay available through phase 3. Only creating assignments and modifying definitions stop on October 31. Still, export now: the CLI command group is scheduled to disappear in November, and the service itself on January 31.

Do I need template specs, or is Git enough? Either works for storing and versioning. Template specs are useful when other teams need to deploy your template from inside Azure without access to your repo. If everything deploys from a pipeline, Git plus az stack ... --template-file is simpler.

What is the minimum if the tenant itself is being decommissioned soon? Microsoft's FAQ says that for a tenant on a near-term decommissioning timeline, exporting plus ordinary Azure resource locks is acceptable. For anything that outlives January 2027, it recommends deployment stacks.

Is there a direct import from a blueprint into a stack? Not that Microsoft documents. The documented path is export, convert to ARM/Bicep, then deploy through a stack. That conversion is why the triage step matters.

Key Takeaways

WhatWhenAction
New assignments blocked, definitions frozenOct 31, 2026Replace vending pipelines that create assignments
az blueprint removed from Azure CLI2.92.0, Nov 2026 (scheduled)Export before upgrading, or pin the CLI version
Assignments can't be modifiedDec 31, 2026Downgrade Read Only locks before this if a stack must write to those resources
Service retired, locks removed, unexported data deletedJan 31, 2027Stacks with deny settings must already be in place
ReplacementNowTemplate spec or Git for storage, deployment stack for deployment and deny settings

What I Could and Couldn't Test

I don't have an Azure subscription with live blueprint assignments to migrate, so be clear about where each part comes from. The triage.sh script was run locally with jq against an export rebuilt from Microsoft's documented artifact samples, including a negative case (literal values only, nothing flagged) and a nested concat(parameters(...)) / artifacts(...) case (both flagged). Every az blueprint, az ts and az stack command and flag above is taken from Microsoft's current documentation and CLI reference, but was not run here. The Bicep file was not compiled. The lock hand-over order in Step 6 is my reasoning from the documented deny-assignment semantics, not a tested procedure. Run az deployment sub what-if and rehearse the hand-over in a non-production subscription before you touch production.

Further Reading