Migrate between Azure subscriptions like a pro

By | November 9, 2020

A guide on how to prepare a resource migration between Azure subscriptions like a pro.

Introduction

This article collects the knowledge I have collected throughout the different migrations I have done between Azure subscriptions.
I have worked with several customers that needed to change their Azure subscription type (plan) to another one. The most common scenario is during a merger or acquisition, and they need to consolidate into one subscription. Another classic example is if a subscription was initially on Pay-As-You-Go licensing, and the company is switching to a CSP or EA agreement.

This article will go through the following topics:

The migration process

Microsoft has made it reasonably easy to move resources between subscriptions within the portal. You define the destination subscription and resource group, and you are good to go.
If you only have a test environment or a small set of resources, the move will be made without any errors or complications. However, if you are dealing with a production workload or a large Azure environment, it can be slightly more complicated.

A simple move

When you are inside a resource group, you can go to “Move” and choose the option called “Move to another subscription.”
Next, you’ll select all the resources you would like to move and choose the destination subscription and resource group.

Migrate between Azure subscriptions like a pro
select resource to migrate
select subscription

And that is how you can migrate between Azure subscriptions the easy way – but you should keep reading…

Why would you want to migrate between Azure subscriptions?

There might be several reasons why you would want to migrate your Azure resources between subscriptions. Maybe you are using an EA Subscription and are going over to a CSP subscription due to billing. Or perhaps you have deployed your resources into the wrong subscription, or your company has been bought and needs to consolidate with a new company.
No matter the reason, you should always plan the move, and look into what resources are possible to move, and if there will be any complications under the migration.

Moving between different Azure subscription models

There is a big difference if you are moving from CSP to EA or CSP to CSP etc.
Some subscription types can be moved seamlessly, without significant effort and time, but others might take a lot of time.

There is also a big difference if you will move all your resources or just a couple of resources.
You can move resources between all kinds of subscriptions using the “Move to another subscription” in the portal (As explained in the previous section).
But imagine if you have a large environment with a lot of resources, then it would be time-consuming to move them all manually.
Moving resources manually can give you complications like downtime for your Vnet peering, but more on that later…

If you want to move your resources, then you should aim for a seamless move. A seamless move means that your cloud partner will handle the billing change behind the scenes without any action on your part.

The migration matrix

Look at the table below to get an overview of which approach needed to move from one subscription type to another.

These are the most common subscriptions types people move resources between:

  • CSP v1
  • EA
  • CSP v2 (Azure Plan)
  • Pay-as-you-go (PAYG)
  • MSDN
NOTE: Microsoft keeps improving the way it can be done. It would be a good idea to check the official documentation now and then.
Source Subscription ModelDestination Subscription ModelMigration Progress
CSP v1CSP v1Back-end billing change. No manually resource migration.
CSP v1EAResources must be manually migrated. I talked with Azure support recently. They mentioned they perhaps have a new service available next year to move resources seamlessly.
CSP v1CSP v2It can only be done by the CSP Partner, who is provided to you.
CSP v1PAYGResources must be manually migrated.
CSP v1MSDNResources must be manually migrated.
EACSP V1Resources must be manually migrated. If you have an EA Subscription type (MS-AZR-0017P), you can contact the support team and request a seamless migration.
EAEAResources must be manually migrated.
EACSP v2It can be done by back-end billing change. https://docs.microsoft.com/en-gb/azure/cost-management-billing/manage/mpa-request-ownership (CSP Direct Bill Partners who are Azure Expert MSP)
EAPAYGResources must be manually migrated.
EAMSDNResources must be manually migrated.
CSP v2CSP v1Not possible
CSP v2EAResources must be manually migrated.
CSP v2CSP v2Back-end billing change. No manually resource migration.
CSP v2PAYGResources must be manually migrated.
CSP v2MSDNResources must be manually migrated.
PAYGCSP v1Resources must be manually migrated.
PAYGEABack-end billing change. No manually resource migration.
PAYGCSP v2Resources must be manually migrated.
PAYGPAYGResources must be manually migrated.
PAYGMSDNResources must be manually migrated.
MSDNCSP v1Resources must be manually migrated.
MSDNEABack-end billing change. No manually resource migration.
MSDNCSP v2Resources must be manually migrated.
MSDNPAYGResources must be manually migrated.
MSDNMSDNResources must be manually migrated.

Things to be aware of when you migrate between Azure subscriptions

If you are in a situation where you have to do a manual migration, there are some things you have to keep in mind. Not all resource types can be moved between subscriptions. For example, CSP does not support Classic resource types, so before you can migrate the resources, you have to convert them into ARM resource types. Some other resources do not support subscription migration. To get an overview of which resource type you can move, see the list from Microsoft. Link

I.e., you have provisioned an Azure VM with a plan(image) from the Azure marketplace that is only supported for a specific subscription type.

If you are getting a validation error on the Azure VM plan for a VM, you can run this CLI Command to see which plan it’s running, and in this case, you will have to recreate the VM in the new subscription.

az vm show -n "vm name"  -g "resource group name" --query "{name:name,plan:plan.name}" -o table
Migrate between Azure subscriptions using powershell

I recommend getting an overview of which resource types you will be moving and check if they are currently supported for migration. And keep in mind that Microsoft keeps updating the different resource types to be supported.

Even though a resource type is supported doesn’t mean it won’t cause downtime to the service, or all the resource features are supported for migration.
For example, you can move a Vnet between subscriptions, but if you have Vnet peering configured, you must disconnect those, and therefore you will have downtime for that service. That is what I meant by complications for resource migration.

How to figure out if there will be any complications in resource migration

When you are making the actual move from the Azure portal, it will validate your resources before moving them to the new subscription. If the validation fails, it will tell you why it fails. Unfortunately, you cannot do these validations in the Azure portal without making the actual move.
You should know if there will be any complications before starting any migration. You could have many resources that will have downtime, and the overall migration time will be longer than you expected. Most importantly, the end customer won’t be happy they weren’t informed.

To overcome this issue, Microsoft has an API for resource validation so that you can request confirmation before making the actual move. Link

How to call the validation API

There are different ways to call an API, some prefer GUI based solutions like Postman, but I prefer doing it through PowerShell. The Powershell validation script is at the end of this article. If you prefer using Postman, I recommend looking into this blogpost.

When calling the API from a PowerShell script, you must specify the following things:

  • Service principal ID
  • Service principal secret
  • Tenant ID
  • Source resource group name
  • Source Subscription id
  • Destination resource group name
  • Destination Subscription id

The Service principal you are using must have access to both source and destination subscriptions, and the subscriptions must be in the same AzureAD tenant.

If the subscriptions are in different AzureAD tenants, look at this article to move subscriptions between tenants.

Keep in mind all RBAC roles will be removed, and the account you are moving with is the only one that will have access after the switchover, so you will have to reestablish all RBAC roles again.

When you check against the validation API, you will go through two phases.
The first phase includes resource providers. If you have not registered the required resource providers to run the resources, the validation will display an error and stop.
You will then have to register the required resource provider and rerun the script before continuing to phase two.
In phase two, it will validate the actual resource and its dependencies.

Status codes

There are two types of status code you will receive when running the script:

Status code 202: This means the request you send is accepted and being processed.

Status code 204: This means there are no complications or errors if you try to move your resources.

Examine the screenshots below for more details:

Migrate powershell output
azure powershell migration output
migrate azure error in powershell

Prepare for a migration

Now that you know how you can validate your resources before doing any migration. You can now make a migration plan and communicate it to the customer or your project manager. They and you will now know what will need to be done before and after the migration to complete it.

The checklist

I have created a list that I use when doing Azure resource migrations. You can use it as an inspiration for your own migration project.

  • Find out what kind of source and destination subscription type you are migrating between.
  • Check if the migration can be done seamlessly.
    • If it can be done seamlessly, get a hold of your subscription partner.
    • Proceed to the next step if migration can’t be done seamlessly.
  • Get an overview of the current environment and which resource types are present. This can be done with the Azure Resource Graph.
  • Be sure that both the source and destination subscription is in the same AzureAD tenant.
    • If not, do a Directory change.
    • RBAC roles will be removed. Make sure to establish them again.
  • Create a Service Principal to run the validation script.
    • The Service Principal needs the RBAC role “Contributor” on both source and destination subscription.
  • Make sure resource groups are created on the destination subscription.
    • You will need those when running the validation script.
      It’s only resources within the resource group that will get moved and not the resource group itself.
  • Run validation script. Remember to set your parameters.
    • You’ll have to register the service provider before getting that actual validation for the resource and its dependencies. But the output on the validation script will tell you what to do.
    • I recommend taking notes and creating a migration plan based on the output you get out of the validation script, especially if you migrate many resources.
      So when you are doing the actual migration, you will have an overview and plan of what to do with the different resources. You can use Excel; Create a collum for source and destination resource group, and note the validation errors. Then you know what to do with each resource group.

Wrapping up

I know it can be a time-consuming task to validate and get an overview before any migration.
But the information in this article will help you if it’s a large environment. It’s also much more professional to have a migration plan and tell the customer or project manager what the expected outcome is and if some resources are unsupported.

PowerShell validation script

The Powershell validation script will be available on my account Github.
Go there for the latest updates (article script will not be synced with the GitHub version).

<# 
.SYNOPSIS 
Function which validates resource move between resource groups across subscriptions. 
 
.DESCRIPTION 
Wrapper function around Azure API https://docs.microsoft.com/en-us/rest/api/resources/resources/validatemoveresources 
Function is building all necessary parts to send a properly formated query as a web request via API and validate resources between resource groups across subscriptions. 
ARM will report if the resources can be migrated or there are dependencies which have to be resolved. 
 
.PARAMETER ApplicationId 
ID of the service principal/application which will be used to validate the resource move, keep in mind that it has to have proper rights. 
 
.PARAMETER ApplicationPassword 
Password of the service principal/application. 

.PARAMETER TenantID 
Tenant ID of the AzureAD Tenant
 
.PARAMETER SourceResourceGroup 
Name of the resource group where resources are located. 

.PARAMETER SourceSubscriptionID
SubscriptionID where resources are located.
 
.PARAMETER DestinationResourceGroup 
Name of the resource group to where you want to move the resources. 

.PARAMETER DestinationSubscriptionID
SubscriptionID to where you want to move the resources. 
 
#> 

#######################
# Start Parameters
#######################
#User Principal Login
$ApplicationId = ""  
$ApplicationPassword = Convertto-SecureString -String "" -AsPlainText -Force

#Tenant ID
$TenantID = ""

#Source details
$SourceSubscriptionID = ""
$SourceResourceGroup = ""

#Destination details
$DestinationSubscriptionID = ""
$DestinationResourceGroup = ""

#######################
# End Parameters
#######################

Function Get-AzAccessToken { 
    [CmdletBinding()] 
    param( 
        [Parameter(Mandatory = $True, 
            ValueFromPipeline = $True, 
            ValueFromPipelineByPropertyName = $True, 
            HelpMessage = 'Provide Client Id or Application Id from Azure AD or any Microsoft API.')] 
        [string]$ClientId, 
        [Parameter(Mandatory = $True, 
            ValueFromPipeline = $True, 
            ValueFromPipelineByPropertyName = $True, 
            HelpMessage = 'Provide Client Secret provided from Azure AD or any Microsoft API.')] 
        [securestring]$ClientSecret, 
        [Parameter(Mandatory = $True, 
            ValueFromPipeline = $True, 
            ValueFromPipelineByPropertyName = $True, 
            HelpMessage = 'Provide Microsoft Azure API Login URL.')] 
        [string]$ApiUri 
    ) 
    process { 
        $GrantType = 'client_credentials' 
        $TargetResource = 'https://management.core.windows.net/' 
        $ClientSecret2 = $([System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($ClientSecret)))
        $body = "grant_type=$GrantType&client_id=$ClientId&client_secret=$ClientSecret2&resource=$TargetResource" 
        $response = Invoke-RestMethod -Method Post -Uri $ApiUri -Body $body -ContentType 'application/x-www-form-urlencoded' 
        return $response 
    } 
} 


Function Get-AzResourceMoveValidation {
    [CmdletBinding()]
    param (
         # ID of the Source Subscription ID.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$SourceSubscriptionID,
        # ID of the Destination Subscription ID.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$DestinationSubscriptionID,
        # ID of the service principal/application.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationId,
        # Password of the service principal.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [securestring]$ApplicationPassword,
        # Name of the source resource group.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$SourceResourceGroup,
        # Name of the target Resource id.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$TargetResourceGroup
    )
    process {
        $TenantId = (Get-AzContext | Select-Object -ExpandProperty Tenant).Id
        $SubscriptionId = $SourceSubscriptionID
        $ApiUrl = "https://login.microsoftonline.com/$TenantId/oauth2/token"
        $TargetUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$SourceResourceGroup/validateMoveResources?api-version=2019-08-01"
        Select-Azsubscription -Subscription $SourceSubscriptionID
        [array]$ResourceList = (Get-AzResource -ResourceGroup $SourceResourceGroup | Where-Object { $_.ResourceType -match '^[^\/]+\/[^\/]+$' }).ResourceId
        $TargetId = "/subscriptions/$DestinationSubscriptionID/resourceGroups/$TargetResourceGroup"
        $CustomObject = [PSCustomObject]@{
            resources           = $ResourceList
            targetResourceGroup = $TargetId
        }
        $Body = $CustomObject | ConvertTo-Json
        $TokenSplat = @{
            ClientId     = $ApplicationId
            ClientSecret = $ApplicationPassword
            ApiUri       = $ApiUrl
        }
        $Token = Get-AzAccessToken @TokenSplat
        $Headers = @{ }
        $Headers.Add("Authorization", "$($Token.token_type) $($Token.access_token)")
        $RestSplat = @{
            Uri         = $TargetUri
            Method      = 'Post'
            Headers     = $Headers
            ContentType = 'application/json'
            Body        = $Body
        }
        try {
            $ErrorActionPreference = 'Stop'
            $Capture = Invoke-WebRequest @RestSplat
            if ($Capture.StatusCode -eq 202) {
                $RestSplat.Uri = "$($Capture.Headers.location)"
                $RestSplat.Method = 'Get'
                $RestSplat.Remove('Body')
                $RestSplat.Remove('ContentType')
                $StartCount = 0
                $RetryCount = 3
                $SleepTimer = 60
                while ($StartCount -ne $RetryCount) {
                    $StartCount++
                    Invoke-WebRequest @RestSplat
                    Start-Sleep -Seconds $SleepTimer
                }
            }
            elseif ($Capture.StatusCode -eq 204) {
                Write-Host "VALIDATION SUCCEEDED, ALL RESOURCES CAN BE MOVED!" -ForegroundColor Cyan
            }
            else {
                $Capture
            }
        }
        catch {
            Write-Error "$_" -ErrorAction Stop
        }


               
    }
}


#Login with access Token
$AccessToken = Get-AzAccessToken -ClientId $ApplicationId -ClientSecret $ApplicationPassword -ApiUri "https://login.microsoftonline.com/$TenantID/oauth2/token"
Login-AzAccount -AccessToken $AccessToken.access_token -AccountId $ApplicationId

#Run validation
Get-AzResourceMoveValidation -ApplicationId $ApplicationId -ApplicationPassword $ApplicationPassword `
-SourceSubscriptionID $SourceSubscriptionID -SourceResourceGroup $SourceResourceGroup `
-DestinationSubscriptionID $DestinationSubscriptionID -TargetResourceGroup $DestinationResourceGroup

Leave a Reply

Your email address will not be published.