Eight subscriptions, nine days: what a tenant migration actually costs
Two people moved just under 5,000 Azure resources into a different tenant. Production took two and a half hours because Dev took two days. The landmines, the verification discipline, and why the first environment is research and development, not delivery.
Two of us moved eight Azure subscriptions into a different tenant. Just under 5,000 resources, nine days end to end, zero rollbacks.
The headline number is not the interesting part. This is:
| Environment | Duration |
|---|---|
| Dev | 2 days |
| Test | 10 hours |
| Stage | 6 hours |
| Production | 2.5 hours |
Identical architecture every time. Production was the fastest run of the four.
That is not a story about getting braver as you approach production. It is a story about what the first environment actually costs, and what it buys.
A tenant migration is not a lift-and-shift
This is the part that surprises people, and it surprised us in the first few hours of Dev.
When you transfer an Azure subscription between tenants, the resources come with it. Virtual machines, storage accounts, databases, function apps: all present, all running, nothing recreated. If you only look at the resource list, the migration looks finished the moment the transfer completes.
The identity layer does not come with it.
Every managed identity is scoped to a directory. Every role assignment points at a principal that exists in that directory. Every key vault access policy names a tenant. Move the subscription and those references survive as text while the things they point at cease to exist from the subscription’s new point of view.
The infrastructure arrives intact and trusts nothing. That is the actual work.
What was in scope
Counted across all eight subscriptions after the move:
| Count | Resource type |
|---|---|
| 639 | private endpoints |
| 299 | storage accounts |
| 241 | function and web apps |
| 126 | resource groups |
| 80 | key vaults |
| 29 | database servers |
| 5 | API Management instances |
Four environments, two platforms: a shared platform stack and an application stack. Just under 5,000 resources total.
The four landmines that cost us the first two days
Dev is where you learn what your plan missed. Ours missed four things, and each one became a scripted step for the three runs that followed.
1. System-assigned identities are stripped on transfer
A system-assigned managed identity belongs to the old directory. After the transfer, some services came back with identity: null while reporting a perfectly healthy provisioning state.
This breaks in a specific and confusing way if your infrastructure code reads that identity back. Terraform configuration that grants a service access to a key vault by referencing identity[0].principal_id cannot resolve anything at all. The plan fails on a required argument being empty, which sends you looking for a state problem when the truth is that the live resource genuinely has no identity anymore.
The fix is to re-mint the identity before anything else runs, then let the grants follow. The lesson is broader: check for a live identity before you trust a plan that depends on one.
2. Encryption keys can outlive the identity that unwraps them
Customer-managed keys are the sharpest edge in a tenant move.
A database or storage account using customer-managed encryption does not hold the key. It holds a reference to a key, plus an identity authorised to unwrap it. If that identity was user-assigned and belonged to the old directory, the resource ends up pointing at a key it can no longer reach. A database in that state reports itself inaccessible, and the server identity shows as an all-zero GUID, which is a memorable way to discover that an identity has been deleted underneath you.
Recovery is not hard once you know what you are looking at: recreate the identity, grant it on the key vault, rebind. Discovering it during a production window would be a very bad hour.
3. Legacy access policies refuse the tenant flip
Key vaults come in two permission models: the older access policies and role-based access control.
Vaults still using access policies carry a list of principals from the old directory, and the tenant flip is rejected while stale entries remain. One vault in our estate had accumulated more than fifty of them over the years.
They have to be cleared before the flip, not after. This one is easy to script and easy to forget.
4. Databases keep trusting the old directory, silently
The trap I would most want someone else to know about.
A PostgreSQL flexible server configured for directory authentication stores the tenant it trusts in its own auth configuration. Transferring the subscription does not update it. The server keeps running, keeps accepting connections, and keeps validating tokens against a directory it no longer belongs to.
Nothing looks broken. Anything authenticating with a directory token fails, and only that.
The fix is to disable directory authentication, set the tenant explicitly, and re-enable it. The general shape of the lesson: any resource that stores a tenant reference in its own configuration needs to be found and corrected deliberately, because nothing will tell you about it.
Never migrate by hand
Everything above became a script.
This is the rule I would keep if I could only keep one. A manual migration cannot be handed off to a colleague, cannot be rerun after you fix the one step that failed, and cannot run under a service account with permissions your own user does not have. Scripts do all three.
That is why Dev cost two days and Production cost two and a half hours. The two days were not migration work. They were writing and debugging the migration, using an environment where being wrong was survivable. Test, Stage and Production ran the same path with the surprises already removed.
Put differently: the first environment is research and development. Treat it as delivery and you will pay for the same discovery four times.
Verification, not hope
“The pipeline finished” is not evidence, and this is where migrations quietly go wrong.
An identity re-minted but never granted, a key re-wrapped but never verified, a role assignment applied at the wrong scope: all of these produce a green run and a broken environment. The failure surfaces days later, usually via someone else’s incident.
So every environment got a comparison pass before anyone called it done. Source against target, resource by resource, identity by identity. Not a spot check.
Zero rollbacks across eight subscriptions. Rollbacks are cheap insurance you hope never to claim. Verification is the part that actually pays out.
Where AI did the work, and where it did not
This was the first migration where I had an agent doing the grunt work alongside me.
It was good at exactly the things that are tedious and verifiable: sweeping eight subscriptions for drift, generating the repetitive per-service configuration, reading long plan output for the one line that mattered, cross-checking an inventory against what the code claimed.
It decided nothing. Fixed scope with cheap validation, let it run. Open-ended judgement about a production cutover, I steer.
That boundary is why it helped rather than hurt. The failure mode of an agent in this kind of work is not that it writes bad scripts, it is that it produces confident output nobody verified. Which is the same failure mode as a human under time pressure, and it is handled the same way: make the verification cheap and automatic, then trust the check rather than the author.
What I would tell someone starting one
- Budget the first environment as research and development, not as delivery. It will take multiples of the others. That is the point.
- Inventory the identity layer before you move anything: system-assigned identities, user-assigned identities, encryption key holders, anything storing a tenant reference in its own configuration.
- Script it. If you did it by hand, you cannot hand it off, rerun it, or run it as a service account.
- Write the verification pass before you need it. “The job finished” is not evidence.
- Rehearse a rollback you have actually executed. A rollback plan you have never run is a wish.
The slowest run is the one that buys all the others.