Azure Governance

Moving Azure Resources Between Resource Groups and Subscriptions Without Breaking Things

Someone created the production SQL database in the wrong resource group. Or finance wants the data team’s workloads billed to a different subscription. Or a proof-of-concept that lived in a sandbox RG is now real and needs to sit beside its siblings. So you open the portal, click Move, and… a dialog warns you that “moving resources may cause downtime” and that “the resource ID of every resource will change.” You hesitate — and you are right to. A resource move in Azure is not a copy and not a redeploy; it is a metadata operation that re-parents existing resources under a new resource group or subscription, keeping the same data and the same regional placement but issuing every moved resource a brand-new resource ID. That ID change is the entire story: anything that referenced the old ID — a role assignment, a lock, a Bicep/Terraform state file, a policy assignment, a saved portal link, an alert rule — now points at a resource that no longer exists at that path.

This article is the implementation guide a senior architect actually follows. We treat a move as a three-act operation: validate (Azure tells you, before touching anything, whether the move is even allowed), move (the operation runs and the source RG/subscription is locked for its duration), and reconcile (you fix everything that referenced the old IDs — RBAC, locks, IaC state, monitoring, dependencies). You will do the real thing end to end three ways — in the portal, with az CLI using the validate-then-move pattern, and via the ARM validateMoveResources/moveResources operations — plus the cross-subscription path and Azure Resource Mover for resources that need orchestration. Every step has expected output, a validation check, and a rollback note, and the option matrices (what can move, what can’t, what breaks, what the error codes mean) are tables you keep open while the move runs.

By the end you will move resources deliberately instead of hopefully: run validate-move first every time, remove resource locks on both source and target before you start, expect role assignments and policy state to not follow the resource, update IaC so your next terraform plan doesn’t try to recreate a resource that simply moved, and know which resources (a handful — VNet-bound, classic, region-pinned) you must redeploy rather than move. The difference between a clean ten-minute move and a half-day outage is almost entirely preparation — and preparation is exactly what a metadata operation rewards.

What problem this solves

Resources end up in the wrong place constantly. A teammate accepted a quickstart’s default RG. A subscription hit a governance boundary and a workload must relocate to one with the right policy or budget. A reorg moved a product to another cost center, and billing follows the subscription, so the resources must follow too. A sandbox graduated to production. None of these are fixable by editing a field — the RG and subscription a resource lives in are part of its identity, encoded in its resource ID, and the only supported way to change them without redeploying is a move.

What breaks when people move blind: a production move runs while a resource lock silently blocks it and the operation half-fails, leaving some resources moved and some not (the resource group is mid-move and read-only for the whole duration, surprising every other engineer). Role assignments scoped at the old RG vanish — the move does not carry RBAC — so the on-call engineer who had Contributor on the old RG suddenly has nothing, mid-incident. Terraform, run by CI the next morning, sees the resource “missing” at its recorded ID and proposes to destroy and recreate it — against a live database. A dependent resource (a NIC’s VNet, a disk’s VM) was left behind because Azure requires coupled resources to move together, and the move was rejected with a cryptic MoveResourcesHaveInvalidState. Each is preventable, and each is a real incident for teams who treated Move as a harmless button.

Who hits this: anyone doing cloud governance, cost allocation, environment promotion, subscription consolidation, or cleaning up after a year of organic growth. It is most painful for teams managing infrastructure as code (state drift), teams with strict locks and policy (lock/RBAC), and anyone moving networking resources (the move-together and not-supported constraints are densest there).

Here is the whole field in one table — the three kinds of move, what changes, and the first thing to check:

Move type What changes What stays the same Source state during move First thing to check
Within a subscription, across RGs Resource ID (RG segment); ownership/RBAC at RG scope Region, data, config, subscription, billing Both RGs read-only Locks on both RGs; resource-type supported?
Across subscriptions (same tenant) Resource ID (sub + RG segment); billing; subscription-scoped policy/RBAC Region, data, config Both RGs read-only Target sub has the RP registered + quota + policy allows it
Across tenants Not a resource move — re-parent the subscription to the new tenant Resources stay put physically n/a Transfer the subscription (billing/EA), then RBAC re-grant

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand the Azure Resource Hierarchy Explained: Subscriptions, Resource Groups and Resources — that a resource lives in exactly one resource group, which lives in exactly one subscription under a management group — because a move only ever changes the RG and/or subscription segment of a resource ID, never the region. You should know how to run az in Cloud Shell, read JSON, and that resource locks (CanNotDelete, ReadOnly) and Azure RBAC assignments are scoped to a management group, subscription, RG, or individual resource. Familiarity with Azure Subscriptions Explained: Types, Billing Boundaries and When to Create a New One helps, since cross-subscription moves change who pays.

This sits in the Governance & Operations track, downstream of resource-organisation design — if you are still deciding how to group resources, read Azure Resource Groups Done Right: Lifecycle, Boundaries and Grouping Strategy first, because the best move is the one you never have to make. It pairs with Azure Resource Locks: Prevent Accidental Deletes and Changes in Production (the most common silent blocker of a move) and Importing Brownfield Azure Resources into Terraform Without Downtime (the same state-reconciliation discipline applies after a move).

You need the action Microsoft.Resources/subscriptions/resourceGroups/moveResources/action on the source, plus write on the target RG/subscription. A quick map of who must clear the path before a move starts:

Layer What must be true before the move Who usually owns it Failure it causes if ignored
Locks (source + target) No ReadOnly/CanNotDelete lock blocking Resource/RG owner Move rejected or half-completed
RBAC (mover) Mover has moveResources/action on source + write on target Subscription owner AuthorizationFailed
Resource provider (target sub) The resource type’s RP is registered in the target subscription Subscription owner Move fails on cross-sub
Policy (target scope) No deny policy that the moved resource would violate Governance team RequestDisallowedByPolicy
Quota (target sub) Target subscription has quota/limit headroom for the type Subscription owner Quota error on cross-sub
Dependencies All coupled resources moved together Resource owner MissingMoveDependentResources

Core concepts

Five mental models make every later step obvious.

A move re-parents metadata; it does not relocate or recreate. The resource’s bits stay exactly where they are physically — same region, same backing storage, same running VM. What changes is the management metadata: which resource group (and possibly subscription) Azure records as the parent. Because the resource is not recreated you keep your data, IPs and configuration — but because the parent changes, the resource ID changes, and a resource ID is the canonical address every other thing in Azure uses to point at it.

The resource ID is the thing that breaks. A resource ID looks like /subscriptions/<sub>/resourceGroups/<rg>/providers/<rp>/<type>/<name>. A within-subscription move rewrites the <rg> segment; a cross-subscription move rewrites both <sub> and <rg>. Anything that stored the old ID now references a path with no resource at it: role assignments, resource locks (recreated, not moved), policy assignments and compliance state, diagnostic settings and alert rules targeting the resource, Bicep/Terraform state, and hard-coded references in other resources. The move succeeds at the platform level; the breakage is everything downstream you must reconcile.

Validation is a real, free, side-effect-free pre-flight. The validateMoveResources action simulates the move and returns success or a specific error without moving anything — checking type support, dependency completeness, target-provider registration, and policy. Running it first is non-negotiable: it converts a guess into a yes/no in seconds. The actual move uses the parallel moveResources action.

Coupling is enforced — some resources must move as a set. Azure refuses to orphan tightly-coupled resources. A virtual machine must move with its disks and network interfaces; a NIC can’t move without its VNet (you move the whole VNet and everything bound to it); a child moves with its parent. The validate step names the missing members — the corollary being that you often move a bigger blast radius than intended, because the dependency graph drags neighbours along.

The source is locked for the duration, and not everything can move. While a move runs the source resource group is read-only — no creates, updates or deletes on any resource in it (even ones not being moved) until it completes. And a meaningful list of types simply cannot be moved: some region-pinned, some classic, some explicitly “not supported.” For those the answer is redeploy at the target and decommission the source — a copy, not a move.

The vocabulary in one table

Pin down every moving part before the deep sections.

Concept One-line definition Where it lives Why it matters to a move
Resource ID Canonical path /subscriptions/…/resourceGroups/…/providers/… Every resource Changes on move → breaks references
Resource group The container a resource lives in In a subscription The RG segment of the ID is rewritten
Subscription Billing + policy + quota boundary Under a management group Cross-sub move changes billing & policy scope
validateMoveResources Side-effect-free move simulation ARM action Your mandatory pre-flight
moveResources The actual move operation ARM action Re-parents the resources
Resource lock CanNotDelete / ReadOnly guard RG, sub, or resource Blocks the move if present on source/target
RBAC assignment A role at a scope MG/sub/RG/resource Does not move; re-grant at the new scope
Resource provider The Microsoft.* namespace for a type Per subscription Must be registered in the target sub
Azure Resource Mover Service that orchestrates dependent moves Regional service For cross-region / complex dependency moves
Move-together set Coupled resources that must move as one The dependency graph Move the whole set or it’s rejected

Move versus redeploy versus leave it

The first real decision is not how to move but whether to. Three outcomes, and the signal for each:

Choose… When Why Cost / effort
Move The type is supported, you want to keep data/IP/config, same region Metadata-only, no data copy, no new resource Low; reconcile IDs afterward
Redeploy The type is unsupported for move, or you also need a region change, or you want a clean IaC re-parent A move can’t change region and won’t help unsupported types Higher; recreate + cut over + decommission
Leave it The “wrong” placement has no real cost/governance impact A move always has a reconciliation tail; don’t pay it for cosmetics Zero; document why

A move cannot change region — the single most common misconception. If your goal is “this VM should be in centralindia instead of westeurope,” a resource move will not do it; you need Azure Resource Mover (which re-creates the resource in the new region, copying data) or a redeploy. A plain move only re-parents within the same region. Keep the line bright: same region → move; different region → Resource Mover or redeploy. (The full plain-move-versus-Resource-Mover decision is in Step 4.)

Step 1 — Validate before you touch anything

Never move without validating first. The validate action tells you in one call whether the move is allowed and, if not, exactly which resource or rule blocks it — and it changes nothing.

Validate in the portal

Select the resource group, tick the resources to move, choose Move → Move to another resource group (or …another subscription). The wizard runs validation automatically and shows a green check or red error per resource before the OK button does anything — which is why the portal is the safest first move for a beginner.

Validate with az CLI

The CLI exposes validation through az resource invoke-action. Build the source resource IDs and the target RG ID, then ask ARM to simulate:

# Collect the IDs you intend to move (example: two resources in the source RG)
SRC_RG=rg-source
TGT_RG=rg-target
SUB=$(az account show --query id -o tsv)

ID1=$(az resource show -g $SRC_RG -n stkvlabdata --resource-type Microsoft.Storage/storageAccounts --query id -o tsv)
ID2=$(az resource show -g $SRC_RG -n nsg-lab --resource-type Microsoft.Network/networkSecurityGroups --query id -o tsv)
TARGET_ID=/subscriptions/$SUB/resourceGroups/$TGT_RG

# Side-effect-free validation — returns nothing on success, an error body on failure
az resource invoke-action \
  --action validateMoveResources \
  --ids "/subscriptions/$SUB/resourceGroups/$SRC_RG" \
  --request-body "{\"resources\":[\"$ID1\",\"$ID2\"],\"targetResourceGroup\":\"$TARGET_ID\"}"

Expected output on success: HTTP 204 No Content (no body) — that silence is the pass. On failure it returns a JSON error with a code and message naming the offending resource (see the error table below). A non-204 means do not move yet.

Validate via the ARM REST/Bicep operation

The same check is the ARM operation POST .../resourceGroups/{source}/validateMoveResources with a body of { resources, targetResourceGroup }. You rarely hand-author this in a Bicep file — a move is an imperative operation, not a declarative resource — but you can drive it from a deployment script or REST. The shape:

{
  "resources": [
    "/subscriptions/<sub>/resourceGroups/rg-source/providers/Microsoft.Storage/storageAccounts/stkvlabdata"
  ],
  "targetResourceGroup": "/subscriptions/<sub>/resourceGroups/rg-target"
}

For a cross-subscription validation, point targetResourceGroup at an RG in the other subscription’s ID and run the call against the source. For large sets the operation is asynchronous: ARM returns 202 Accepted with a Location header you poll until it resolves to 204 (success) or an error.

The three ways to validate, side by side:

Method How you invoke it Success signal Best for
Portal wizard Move blade runs it automatically Green check per resource First-timers; small ad-hoc moves
az resource invoke-action validateMoveResources One CLI call with IDs + target HTTP 204 (no body) Scripted/repeatable moves, CI gates
ARM REST validateMoveResources POST with JSON body, poll Location 204 after async poll Large sets; pipeline automation

Validation is only the can-I-move gate — it confirms the move is allowed but says nothing about downstream breakage (RBAC won’t survive, IaC state will drift, alerts/links go stale). The reconciliation steps below are the what-breaks-after work.

Step 2 — Clear the blockers (locks, RBAC, providers, policy)

Validation flags most of these, but clear them proactively so the real move runs clean.

Remove resource locks on source and target

A ReadOnly or CanNotDelete lock anywhere on the source RG, the target RG, or the individual resources will block or partially fail the move (a move both deletes-from-source and creates-at-target conceptually, so both lock types bite). List locks, note them so you can reapply, then remove:

# Find every lock in the source RG (and check the target RG too)
az lock list --resource-group rg-source -o table
az lock list --resource-group rg-target -o table

# Delete a blocking lock (record name/level/notes first so you can reapply after)
az lock delete --name lock-prod-delete --resource-group rg-source
// After the move, reapply the lock at the NEW scope (target RG) via IaC
resource rgLock 'Microsoft.Authorization/locks@2020-05-01' = {
  name: 'lock-prod-delete'
  scope: resourceGroup()        // deploy this into the target RG
  properties: {
    level: 'CanNotDelete'
    notes: 'Prod data — reapplied after move 2026-06-24'
  }
}

Locks do not travel with the resource — a lock is a separate resource at a scope, so after the move you must reapply it at the target. Treat lock removal as a deliberate, time-boxed window, not a casual delete.

Confirm your RBAC and that it won’t follow

You need moveResources/action on the source and write on the target. Check your effective permission, and accept the hard truth that role assignments do not move:

# Do you have the move action on the source RG?
az role assignment list --assignee $(az ad signed-in-user show --query id -o tsv) \
  --resource-group rg-source -o table

# After the move, RBAC at the OLD RG is gone for the moved resources — re-grant at the target
az role assignment create --assignee <principal-id> \
  --role "Contributor" --resource-group rg-target

Note every role assignment scoped at the source RG/resource before you move, so you can recreate the meaningful ones at the target. This is the step most teams forget — and how an on-call engineer loses access mid-move.

Register the resource provider in the target subscription (cross-sub only)

For a cross-subscription move, the resource type’s resource provider must be registered in the target subscription, or the move fails. Register it ahead of time:

# Example: target subscription must have the Storage RP registered before a storage move
az account set --subscription <target-sub-id>
az provider register --namespace Microsoft.Storage
az provider show --namespace Microsoft.Storage --query registrationState -o tsv   # want: Registered

Check target policy won’t deny the move

A deny Azure Policy effect at the target subscription or management group (e.g. “deny storage accounts without a required tag” or “deny resources outside an allowed region”) rejects the incoming resource with RequestDisallowedByPolicy. Validation catches this; pre-check the target scope’s policies and either make the resource compliant first or get an exemption.

The blocker checklist as a table — clear every row before Step 3:

Blocker How to detect How to clear Reapply after?
Lock on source RG/resource az lock list -g rg-source az lock delete Yes — at target
Lock on target RG az lock list -g rg-target az lock delete Yes
Missing move action (RBAC) az role assignment list Grant moveResources/action n/a
RP not registered (target sub) az provider show --query registrationState az provider register n/a
deny policy at target Validate error RequestDisallowedByPolicy Fix compliance / exemption n/a
Quota at target (cross-sub) Validate / quota blade Request quota increase n/a
Missing dependency in set Validate error names it Add the dependent to the move n/a

Step 3 — Run the move

With validation green and blockers cleared, the move itself is anticlimactic — the goal.

Move across resource groups (within a subscription)

# Move the validated resources to the target RG in the same subscription
az resource move \
  --destination-group rg-target \
  --ids "$ID1" "$ID2"

Expected: the command blocks while it runs (seconds to a few minutes for a small set), then returns the moved resources’ new representations. During this window both RGs are read-only. Validate the result:

# The resources now live in the target RG with NEW resource IDs
az resource list -g rg-target --query "[].{name:name, type:type}" -o table
az resource list -g rg-source --query "[].{name:name, type:type}" -o table   # should no longer list them

Move across subscriptions (same tenant)

Add --destination-subscription-id. The lock window is typically longer, and everything in Step 2 (RP registration, policy, quota in the target sub) matters:

az resource move \
  --destination-subscription-id <target-sub-id> \
  --destination-group rg-target \
  --ids "$ID1" "$ID2"

Expected: a longer-running operation; on completion the resources appear under the target subscription/RG and billing shifts to the target subscription from the move time. Verify the subscription context:

az resource show --ids <new-resource-id> --query "{name:name, id:id}" -o json
# The id now contains the target subscription and RG segments

What each move command maps to

Goal az command Key flag Underlying ARM action
RG → RG, same sub az resource move --destination-group moveResources
Sub → Sub, same tenant az resource move --destination-subscription-id + --destination-group moveResources (cross-sub)
Validate first (always) az resource invoke-action --action validateMoveResources validateMoveResources
Region change Azure Resource Mover n/a (separate service) Microsoft.Migrate/moveCollections

A move of a single set is all-or-nothing — if it fails partway ARM rolls it back to source — while independent moves are separate transactions. Move related resources as one --ids list.

Step 4 — Use Azure Resource Mover for complex or cross-region moves

When the move involves a region change, or a large web of dependent resources you don’t want to assemble by hand, Azure Resource Mover is the right tool. It is a distinct service (not the moveResources action): you create a move collection, add resources, let it auto-discover dependencies, then run Prepare → Initiate Move → Commit (or Discard) as explicit, resumable phases. Unlike a plain move it creates new resources and copies data — exactly why it can cross regions where a metadata-only move cannot.

# Resource Mover lives under the 'az resource-mover' extension; install it first
az extension add --name resource-mover

# Create a move collection (the orchestration unit), then add resources to it
az resource-mover move-collection create \
  --name mc-relocate --resource-group rg-mover \
  --source-region westeurope --target-region centralindia \
  --location westeurope --identity type=SystemAssigned

The plain-move-versus-Resource-Mover decision, made concrete:

If you need to… Use Why
Re-parent same-region resources to another RG az resource move Metadata-only, fastest, no data copy
Re-parent same-region resources to another subscription az resource move --destination-subscription-id Same, plus billing shifts
Move resources to a different region Resource Mover Plain move can’t change region
Move a VM with all its disks/NICs/VNet across region Resource Mover Auto-sequences the dependency graph
Stage a move you can rehearse and roll back in phases Resource Mover Prepare/Initiate/Commit/Discard lifecycle

Resource Mover’s phase model is its safety feature — the reason to prefer it for anything high-stakes:

Phase What it does Reversible?
Add resources Stages resources + auto-discovers dependencies into a collection Yes (remove)
Prepare Creates the move infrastructure; validates readiness Yes
Initiate move Creates the target resources (data copied for cross-region) Yes (via Discard)
Commit Finalises; you then delete the source No (after commit, clean up source)
Discard Rolls back an initiated move, deleting the target copies n/a (this is the rollback)

Architecture at a glance

Walk the diagram left to right as the lifecycle of a single move. The operator issues the request — portal, az resource move, or a pipeline — to Azure Resource Manager (ARM), the control plane that owns every resource ID and is the only thing that can re-parent one. ARM first runs the validateMoveResources pre-flight (the dashed loop): it inspects the source resource group and confirms type-support, an intact move-together set, target-provider registration, and that no deny policy or lock stands in the way. Only on a clean validation does ARM execute moveResources, re-parenting the selected resources into the target resource group (and, on a cross-subscription move, a different subscription with its own billing and policy scope). Throughout, both RGs are flipped read-only.

The right-hand zone is the part everyone forgets: reconciliation. The moved resource now has a new ID, so the operator must re-grant RBAC at the new scope, reapply locks, and update IaC state so the next plan doesn’t recreate it. The numbered badges mark the four places a move goes wrong — a lock on the source (1), a missing dependency (2), a deny policy at the target (3), and IaC drift afterward (4) — each narrated in the legend as symptom → confirm → fix. Follow the arrows and you have the whole method: validate, clear blockers, move, reconcile.

Left-to-right Azure resource-move architecture: an operator (portal, az CLI or pipeline) sends a request to Azure Resource Manager, which runs a validateMoveResources pre-flight against the source resource group (checking type support, the move-together dependency set, target resource-provider registration, and deny policies/locks) before executing moveResources to re-parent the resources into a target resource group in the same or a different subscription while both groups are read-only, then a reconciliation zone re-grants RBAC, reapplies locks, and updates Bicep/Terraform state — with numbered failure badges on the source lock, a missing dependency, a target deny policy, and post-move IaC drift

Real-world scenario

Northwind Analytics runs a data platform on Azure: an Azure SQL server and database, a storage account (the data lake), a Key Vault, two app services, and the VNet tying them together — about 30 resources, all in rg-data-poc in their Sandbox subscription, all in Central India. The platform started as a proof-of-concept; eighteen months later it is the revenue-reporting backbone. Finance now needs it billed to the new Data-Platform production subscription (the right budget, tag policy, and CanNotDelete lock standard), and the team wants it in a properly named rg-data-prod. Same region, real data, zero appetite for downtime. Three engineers; ~₹95,000/month.

Their first instinct was to redeploy everything from Bicep into the new subscription and cut over — clean, but it meant copying terabytes of lake data and a risky connection-string cutover. The architect pushed back: same region, mostly supported types, so a move keeps the data in place and avoids the copy entirely. They committed to the validate-first discipline.

Validation immediately earned its keep. The first validateMoveResources against the whole set failed with MissingMoveDependentResources — they had selected the SQL database but not its parent server, and the app services’ VNet-integration NICs without the VNet. They expanded to the full move-together groups and re-validated: now it failed with RequestDisallowedByPolicy, because the production subscription had a deny policy requiring a CostCenter tag the POC resources lacked. They tagged the resources and got the silent 204 — then a third surprise: the production subscription had never used the Microsoft.Sql provider, so it wasn’t registered, and az provider register --namespace Microsoft.Sql cleared it. None of these would have been visible without validation; all three would have been a half-failed production move at 2am.

The move ran in two transactions: the networking set (VNet + dependents) first, then the data + compute set. The architect removed the source RG’s CanNotDelete lock for the window (reapplying a fresh one at the target afterward), warned the team that both RGs would be read-only for ~15 minutes, and ran the cross-subscription az resource move. The catch they planned for: role assignments did not follow — their Contributor grants were scoped at the old rg-data-poc, so on completion they had no access until they re-ran their RBAC-as-code at rg-data-prod. Because they expected it, the gap was 90 seconds, not an incident.

The tail was Terraform: the next morning’s CI plan proposed to destroy and recreate the storage account and SQL database, seeing them “missing” at their recorded IDs. The engineer terraform state mv’d each moved resource to its new ID and re-pointed the resource_group_name/subscription in the config; the following plan showed no changes. Customer-visible downtime: zero — resources never stopped; only the control plane re-parented them. The lesson on the wall: “A move is 10% the move and 90% reconciling everything that pointed at the old ID — validate first, and write down what you’ll have to fix.”

The move as a timeline, because the order is the lesson:

Phase Action Result What it prevented
Plan Chose move over redeploy (same region) No data copy of TBs Days of lake-data egress + risk
Validate #1 validateMoveResources whole set MissingMoveDependentResources A half-failed move (orphaned DB/NICs)
Validate #2 After expanding the set RequestDisallowedByPolicy (tag) Rejected production move at runtime
Prep Tagged resources; registered Microsoft.Sql RP Clean 204 on re-validate Cross-sub provider failure
Move Removed source lock; 2 transactions Re-parented, ~15 min read-only Lock-blocked partial move
Reconcile Re-granted RBAC at target; reapplied lock 90-second access gap On-call lockout mid-move
IaC terraform state mv + re-import Next plan: no changes Destroy/recreate of live DB

Advantages and disadvantages

A metadata move is the right tool surprisingly often, but it has a real reconciliation tail. Weigh it honestly:

Advantages (why move beats redeploy) Disadvantages (why it bites)
No data copy — terabytes stay in place; the move is metadata-only The resource ID changes, breaking every downstream reference (RBAC, locks, IaC, alerts)
No application downtime — the running resource never stops Both RGs go read-only for the duration — surprising every other engineer touching them
Keeps IPs, config, secrets, endpoints — no cutover of connection strings RBAC and locks don’t follow — you must re-grant/reapply, risking an access gap
Fast — seconds to minutes for typical sets A move-together set can drag a far larger blast radius than you intended
Validate-first gives a definitive yes/no before any change A meaningful list of types can’t move at all — you still need a redeploy plan for those
Cross-sub move shifts billing cleanly from the move time Cannot change region — the most common thing people think a move does
Same supported pattern in portal, CLI, and ARM IaC drift is near-guaranteed and silent until the next plan proposes a destroy

The model is right when the type is supported, the region stays the same, and you value keeping data/IP/config in place over a clean-slate redeploy. It bites hardest on IaC-managed estates (state drift), strict-governance subscriptions (locks/policy/RBAC), and networking-heavy sets (move-together density). Every disadvantage is manageable — but only if you anticipate it, which is the point of validating and writing the reconciliation list before you click Move.

Hands-on lab

This is the centerpiece. You will create two resource groups and a small set of resources, validate a move, move the resources across RGs, verify, and reconcile locks/RBAC — then tear it down. It is free-tier-friendly: the only billable resource is a tiny storage account that costs pennies. Run everything in Cloud Shell (Bash).

Part A — Set up the source

Step 1 — Variables and two resource groups.

LOC=centralindia
SRC_RG=rg-move-source
TGT_RG=rg-move-target
SUB=$(az account show --query id -o tsv)
SA=stmovelab$RANDOM        # storage names: 3–24 chars, lowercase+digits, globally unique

az group create -n $SRC_RG -l $LOC -o table
az group create -n $TGT_RG -l $LOC -o table

Expected: two RG rows, provisioningState = Succeeded. The target RG must exist before the move — a move re-parents into an existing RG, it doesn’t create one.

Step 2 — Create movable resources in the source RG. A storage account and a network security group (NSG) — both fully supported for move, and the NSG lets you see a networking resource move cleanly:

az storage account create -n $SA -g $SRC_RG -l $LOC --sku Standard_LRS -o table
az network nsg create -n nsg-movelab -g $SRC_RG -l $LOC -o table

# Capture their resource IDs — these are what you validate and move
ID_SA=$(az storage account show -n $SA -g $SRC_RG --query id -o tsv)
ID_NSG=$(az network nsg show -n nsg-movelab -g $SRC_RG --query id -o tsv)
echo "$ID_SA"; echo "$ID_NSG"

Expected: both IDs printed, each containing /resourceGroups/rg-move-source/ — the RG segment about to change.

Part B — Validate (the mandatory pre-flight)

Step 3 — Run validateMoveResources and read the result.

TARGET_ID=/subscriptions/$SUB/resourceGroups/$TGT_RG

az resource invoke-action \
  --action validateMoveResources \
  --ids "/subscriptions/$SUB/resourceGroups/$SRC_RG" \
  --request-body "{\"resources\":[\"$ID_SA\",\"$ID_NSG\"],\"targetResourceGroup\":\"$TARGET_ID\"}"

Expected on success: no output and a zero exit code (HTTP 204). That silence is the green light. If you instead see a JSON error, read its code against the error table later in this article and fix the cause before proceeding — do not move on a failed validation.

Step 4 — (Optional) Prove validation catches a real problem. Put a ReadOnly lock on the source RG and re-validate to see it fail, then remove it:

az lock create --name lab-readonly --resource-group $SRC_RG --lock-type ReadOnly
# Re-run the Step 3 validate command — it now reports the lock as a blocker
az lock delete --name lab-readonly --resource-group $SRC_RG

This is the single most valuable habit the lab teaches: validation turns “I hope this works” into a definite answer in seconds.

Part C — Move and verify

Step 5 — Move both resources to the target RG (one transaction).

az resource move \
  --destination-group $TGT_RG \
  --ids "$ID_SA" "$ID_NSG"

Expected: the command blocks for a few seconds to a minute, then returns the resources’ new representations. While it runs, both RGs are read-only — try az storage account create in either and it will be rejected until the move finishes (don’t actually leave one running; just know it would fail).

Step 6 — Verify the move landed and the IDs changed.

# Target RG now lists both resources
az resource list -g $TGT_RG --query "[].{name:name, type:type}" -o table

# Source RG no longer lists them
az resource list -g $SRC_RG --query "[].{name:name, type:type}" -o table

# The storage account's ID now contains the TARGET RG segment
az storage account show -n $SA -g $TGT_RG --query id -o tsv

Expected: the target list shows both resources; the source list is empty; the new ID contains /resourceGroups/rg-move-target/. Same resource, same data, new address.

Part D — Reconcile (the part that matters)

Step 7 — Reapply a lock at the new scope. Locks didn’t move; recreate the protection on the target:

az lock create --name prod-nodelete --resource-group $TGT_RG --lock-type CanNotDelete \
  --notes "Reapplied after move $(date +%F)"
az lock list -g $TGT_RG -o table

Step 8 — Re-grant RBAC at the new scope (illustrative). If a principal had a role at the old RG, recreate it at the target. (Substitute a real principal ID; this shows the pattern.)

# Example pattern — the move did NOT carry any role assignments from rg-move-source
# az role assignment create --assignee <principal-id> --role "Reader" --resource-group $TGT_RG
az role assignment list --resource-group $TGT_RG -o table   # confirm what's (not) there

Step 9 — (Bicep) Reapply governance as code at the target. In a real estate you express the target-side governance declaratively so it is reproducible. A minimal Bicep deployed into the target RG:

// deploy with: az deployment group create -g rg-move-target -f reconcile.bicep
targetScope = 'resourceGroup'

@description('Reapply a delete-lock and a baseline role at the post-move scope.')
resource lock 'Microsoft.Authorization/locks@2020-05-01' = {
  name: 'prod-nodelete'
  properties: {
    level: 'CanNotDelete'
    notes: 'Reapplied after resource move'
  }
}

Validation checklist. You created resources, validated a move (and watched a lock make validation fail), moved both resources in one transaction, confirmed the new IDs, and reapplied a lock at the target. The lab steps mapped to what each proves:

Step What you did What it proves Real-world analogue
3 validateMoveResources → 204 Validation is a real, free pre-flight The mandatory first move every time
4 Lock → validate fails → unlock Locks silently block moves The #1 cause of a half-failed move
5 az resource move (one --ids list) A set moves as one transaction Moving coupled resources together
6 Verify new IDs / empty source The resource ID is what changes Why every downstream reference breaks
7 Reapply lock at target Locks don’t travel with the resource Re-securing prod after a move
8 Inspect RBAC at target Role assignments don’t follow Avoiding the on-call lockout

Teardown

# Delete both resource groups (removes the storage account, NSG, and locks)
az group delete -n $TGT_RG --yes --no-wait
az group delete -n $SRC_RG --yes --no-wait

If a lock blocks the delete, remove it first (az lock delete), then re-run. Cost note: an empty Standard_LRS storage account costs a negligible amount per hour; an hour of this lab is well under ₹5, and deleting both RGs stops everything.

Common mistakes & troubleshooting

The failure modes you will actually hit, as a scannable playbook first, then the high-bite ones expanded.

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 Move rejected immediately; mentions a lock ReadOnly/CanNotDelete lock on source or target az lock list -g rg-source; az lock list -g rg-target Delete the lock; move; reapply at target
2 MissingMoveDependentResources / MoveResourcesHaveInvalidState A move-together member (VNet, SQL server, disk, NIC) was left out Read the validate error — it names the missing resource Add the dependent to the same --ids list
3 RequestDisallowedByPolicy A deny policy at the target scope blocks the resource Validate error; target policy assignments blade Bring resource into compliance (tag/region) or get an exemption
4 Cross-sub move fails on a type Resource provider not registered in target subscription az provider show -n Microsoft.X --query registrationState az provider register --namespace Microsoft.X
5 AuthorizationFailed on the move Mover lacks moveResources/action on source or write on target az role assignment list --assignee <you> -g rg-source Grant the move action / Contributor at the right scope
6 “This resource type can’t be moved” The type is unsupported for move (or unsupported cross-sub) Validate error; the move-support docs/table Redeploy at target; decommission source
7 Next terraform plan wants to destroy/recreate the resource IaC state still references the old resource ID terraform plan shows -/+ on a moved resource terraform state mv / re-import at new ID; update config
8 An engineer can’t touch a resource mid-move Source RG is read-only for the whole move duration Activity log shows the move in progress Wait for the move to complete; communicate the window
9 Lost access to the resource right after the move Role assignment was scoped at the old RG; didn’t follow az role assignment list -g rg-target shows it missing Re-grant RBAC at the new scope (ideally as code)
10 Alerts/diagnostic settings stopped firing after move They targeted the old resource ID Alert rule scope / az monitor diagnostic-settings list Re-point the alert/diagnostic to the new ID
11 Wanted the resource in another region; move “did nothing” to region A move never changes region The resource’s location is unchanged Use Azure Resource Mover or redeploy in the target region
12 Move appears stuck for many minutes Large set / cross-sub move is genuinely slow, or async-pending Activity log; poll the operation status Wait; don’t re-issue (you’ll double-submit); check for a partial rollback

The entries that bite hardest, expanded:

1. The lock you forgot. A ReadOnly/CanNotDelete lock on the source RG, target RG, or any resource in the set blocks the move (which both removes-from-source and creates-at-target). Confirm: az lock list -g rg-source and -g rg-target. Fix: record the lock, delete it, move, then reapply an equivalent lock at the target — locks never travel with the resource. See Azure Resource Locks: Prevent Accidental Deletes and Changes in Production.

2. The dependency you left behind. Azure rejects a move that would split a move-together set: a SQL database without its server, a NIC without its VNet, a VM without its disks. Confirm: the validate error names the missing member. Fix: add every member of the set to the same --ids list — expect a larger blast radius than your original selection.

3. The policy that says no. A deny effect at the target subscription or management group rejects the incoming resource. Confirm: validate returns RequestDisallowedByPolicy. Fix: make the resource compliant (required tag, allowed region/SKU) or arrange an exemption — common when moving from a loose sandbox into a strict production subscription.

4. The unregistered provider. Cross-subscription moves require the type’s provider registered in the target subscription; one that has never used Microsoft.Sql won’t have it. Confirm: az provider show -n Microsoft.Sql --query registrationState returns NotRegistered. Fix: az provider register --namespace Microsoft.Sql and wait for Registered.

7. The Terraform destroy-and-recreate. The most dangerous tail. State recorded the resource at its old ID; after the move it’s “missing,” so the next plan proposes to destroy the now-orphaned-in-state resource and create a new one — against live infrastructure. Confirm: terraform plan shows a -/+ on a resource you only moved. Fix: terraform state mv (or remove + re-import) to the new ID and re-point resource_group_name/subscription, so the next plan is clean — the same discipline as Importing Brownfield Azure Resources into Terraform Without Downtime.

The move-failure error codes, as a reference:

Error code Meaning Likely cause First fix
MissingMoveDependentResources A required dependent wasn’t included Left a VNet/server/disk out of the set Add the named dependent to the move
MoveResourcesHaveInvalidState A resource is in a state that can’t be moved Provisioning/failed/locked resource Resolve the resource’s state; remove locks
RequestDisallowedByPolicy A deny policy blocked it Target policy (tag/region/SKU) violated Make compliant or get an exemption
ResourceMoveProviderNotRegistered Target sub doesn’t have the RP Provider never used in target sub az provider register --namespace …
AuthorizationFailed Caller lacks permission No moveResources/action or target write Grant the right role at the right scope
ScopeLocked / lock errors A lock blocks the operation ReadOnly/CanNotDelete on src/target Remove the lock; reapply after
MoveResourcesNotSupported The type can’t be moved (here) Unsupported type or unsupported cross-sub Redeploy at target instead

Best practices

Security notes

Cost & sizing

A resource move is free — no charge for the moveResources operation, and because it is metadata-only you copy no data and provision nothing new. The cost story is entirely second-order:

A rough picture for the three move types:

Move type Direct Azure cost Second-order cost When the bill actually moves
RG → RG, same sub Free None Billing unchanged (same sub)
Sub → Sub, same tenant Free None (no data copy) Target sub billed from move time
Cross-region (Resource Mover) Free orchestration, but… Duplicate resources + data egress during the window Target region from cutover; delete source to stop double-billing

There is nothing to “size” in a move itself, but batch sensibly — 200 resources in one set means a longer read-only window than a few logical sets. Group by dependency (each move-together set as one transaction) and acceptable freeze window, not all-at-once.

Interview & exam questions

1. What actually changes when you move an Azure resource to a different resource group, and why does it break things? The resource’s resource ID changes (the RG — and, cross-sub, the subscription — segment is rewritten); the resource itself is not recreated, so data, region, IPs, and config are preserved. It “breaks things” because everything that stored the old ID — role assignments, locks, IaC state, alerts, diagnostic settings — now points at a path with no resource, and must be reconciled.

2. Can a resource move change the resource’s region? No. A move only re-parents within the same region. To change region you use Azure Resource Mover (which recreates the resource in the new region and copies data) or you redeploy. This is the most common misconception about moves.

3. Why must you run validateMoveResources first, and what does a successful validation look like in the CLI? Validation is a side-effect-free simulation that checks type support, dependency completeness, target-provider registration, and target policy — turning a guess into a definite yes/no and naming the blocker on failure. In the CLI (az resource invoke-action --action validateMoveResources) success is HTTP 204 with no body — silence is the pass.

4. Do role assignments and resource locks move with the resource? No to both. RBAC assignments scoped at the source RG/resource do not follow — you must re-grant them at the new scope. Locks are separate resources at a scope and are not moved — you must reapply them at the target after the move. Forgetting either is how people get locked out or leave production unprotected.

5. You move a SQL database but the move is rejected with MissingMoveDependentResources. What happened? The database is part of a move-together set and you left out a required member — here, its parent logical server (and you must move all databases on that server together). Add the server (and siblings) to the same move operation and re-validate. The same applies to NICs without their VNet and VMs without their disks.

6. What extra prerequisite does a cross-subscription move have that a same-subscription move doesn’t? The resource type’s resource provider must be registered in the target subscription (az provider register --namespace Microsoft.X), the target must satisfy any policy and quota constraints, and billing shifts to the target subscription. A same-sub RG move has none of these because the subscription context is unchanged.

7. After a move, your next terraform plan wants to destroy and recreate the moved resource. Why, and how do you fix it without downtime? Terraform state still records the resource at its old ID, so it appears “missing” and the plan proposes to recreate it. Fix it by updating state to the new ID (terraform state mv, or remove and re-import) and re-pointing resource_group_name/subscription in the config, so the following plan shows no changes — never apply the destroy.

8. What is the state of the source resource group while a move is running? Read-only for the entire duration — no create/update/delete on any resource in it (including resources not being moved) until the operation completes. Communicate this window, and schedule moves during low activity, because it freezes everyone working in that RG, not just the resources you’re moving.

9. When do you use Azure Resource Mover instead of a plain az resource move? When you need a region change, a large dependency graph auto-discovered and sequenced, or a rehearsable, rollback-able move via its Prepare → Initiate → Commit → Discard phases. Plain move is metadata-only and same-region; Resource Mover creates new resources and copies data, which is what lets it cross regions.

10. A move into your production subscription fails with RequestDisallowedByPolicy. What’s the fix? A deny Azure Policy at the target scope (requiring a tag or an allowed region/SKU) is blocking the resource. Make it compliant before moving or arrange an exemption — common when moving from a sandbox into a strict production subscription.

11. Are resource moves atomic? A move of a single set (one --ids list) is all-or-nothing — on failure ARM rolls that set back to the source. But independent moves are separate transactions, so one can succeed while another fails. Move coupled resources as one set to share a transaction.

12. Which permission do you need on the source to perform a move? The action Microsoft.Resources/subscriptions/resourceGroups/moveResources/action on the source (via Contributor/Owner at the right scope), plus write on the target RG/subscription. Lacking either yields AuthorizationFailed.

These map to AZ-104 (Administrator)manage Azure resources, resource groups, locks, RBAC and moving resources — most directly, with the governance/policy angle touching AZ-305 (Solutions Architect) and the IaC-reconciliation discipline relevant to anyone working with Bicep/Terraform. A compact cert-mapping:

Question theme Primary cert Exam objective area
Move across RG/subscription; validate-first AZ-104 Manage Azure identities & governance / resources
Locks, RBAC re-grant, read-only window AZ-104 Manage resource groups, locks, RBAC
Policy deny at target, governance impact AZ-104 / AZ-305 Implement & manage governance
Cross-region move, Resource Mover, dependencies AZ-305 Design migrations / resource organization
IaC drift after move (state reconcile) (role-agnostic) Bicep/Terraform operational hygiene

Quick check

  1. You move a resource to a new resource group. Name the one thing about the resource that changes and the category of things that consequently break.
  2. True or false: a resource move is the correct way to change a VM’s region from westeurope to centralindia.
  3. In the az CLI, what does a successful validateMoveResources call return — and why is that the signal to proceed?
  4. After a cross-subscription move, your on-call engineer suddenly can’t manage the resource. What didn’t follow the move, and what’s the fix?
  5. Your validateMoveResources fails with MissingMoveDependentResources on a SQL database. What did you most likely leave out?

Answers

  1. The resource ID changes (the resource-group — and, cross-sub, subscription — segment is rewritten). Consequently, everything that referenced the old ID breaks: role assignments, locks, IaC state, alert rules, and diagnostic settings. The resource’s data, region, and config are preserved.
  2. False. A move only re-parents within the same region; it cannot change region. Use Azure Resource Mover (recreates + copies data across regions) or redeploy.
  3. HTTP 204 with no body — silence. Validation is side-effect-free, so a clean 204 means Azure has confirmed type support, the move-together set, target-provider registration, and policy all pass, and the actual move is safe to run.
  4. RBAC role assignments did not follow — they were scoped at the old RG/resource and a move doesn’t carry them. Fix by re-granting the engineer’s role (least-privilege) at the new scope, ideally as code so the gap is seconds, not an incident.
  5. Its parent logical SQL server (and the other databases on that server) — they form a move-together set. Add the server and all its databases to the same move and re-validate.

Glossary

Next steps

You can now move resources deliberately — validate, clear blockers, move, reconcile. Build outward:

AzureResource GroupsSubscriptionsResource MoverGovernanceaz CLIBicepMove Resources
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading