If you find yourself needing to move resources in Azure, whether between resource groups or subscriptions, you may find the Portal doesn’t offer a very good validation tool to build a plan for a large migration. While the move itself will perform a validation, if it succeeds it will proceed immediately without further input.
I’m working on a large resource move project, with many inter-connected resources and I needed a method to build a migration plan while having confidence that it will succeed.
A few things to keep in mind about resource moves:
- You cannot move resources between Tenants (Azure Active Directory). You will need to perform a “Change Directory” operation on your source subscription into the new Tenant first, and then do your move validation.
- You cannot move resources with dependencies in different resource groups. So if you have your VMs and NICs in resource group A, and your VNET in resource group B, you’re going to have to consolidate them into a single resource group
- Similarly, if a dependent resource exists but is not moveable, you’re going to have to deal with it. If an Application Gateway exists in a subnet in a VNET, if you try to move the VNET it will fail even if you aren’t trying to move the Application Gateway. It must be deleted instead.
While browsing the Move Resources documentation page, I learned of the “validate move operation” that allows you to make a REST API call to kick-off a validation job, and then another to view the results of that job.
This requires a Bearer access token, so I’m using the same method I did when building my Update Management API call, from this TechNet Gallery post (which has since been updated with Az module support, nice!).
This script assumes that you’re already authenticated to Azure in a PowerShell session, in order to use the method for bearer access token above.
In my example below, I’m gathering all resources within a resource group, and excluding some specific ones from consideration. Instead you could supply individual resource ids (as the Microsoft Doc provides examples of) if you have a simple validation to do.
As the documentation link above describes, you first initiate a validation job, and then you check the status and output of that job afterwards, using the Location URL that is provided in the 202 response. In my example below, I add a sleep timer for 30 seconds, but that may not be enough time to wait.
# https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-move-resources#validate-move # Provide your source subscription id $subscriptionid = "sourcesubid" #Provide the Resource Group Name containing your source resources $resourcegroup = "source resource group name" # Provide your target subscription id (could be the same as source) $targetsub = "targetsubid" # Provide your target resource group name (it must exist in advance) $targetrgname = "target resource group name" # Select the source subscription, so we can dynamically get a list of resources Select-AzSubscription $subscriptionid # Get all resources within the source resource group $resourceids = get-azresource -resourcegroupname $resourcegroup # Remove resource types that we know are incompatible with the move operation. # We will delete these resources before migration, so lets remove them from the object array $resourceids = $resourceids | Where-Object { $_.ResourceType ` -notlike "*/configurations" ` # Validation throws errors of 'Not top-level resource', because this is contained within Automation Account -and $_.ResourceType -notlike "*/runbooks" ` # Validation throws errors of 'Not top-level resource', because this is contained within Automation Account -and $_.ResourceType -notlike "*/extensions*" ` # Validation throws errors of 'Not top-level resource', because this is contained within Virtual Machine -and $_.ResourceType -notlike "*/applicationgateways*" ` -and $_.ResourceType -notlike "*/publicipaddresses*" ` -and $_.ResourceType -notlike "*/vaults*" ` -and $_.ResourceType -notlike "*/connections*" ` # Excluding Connections because they're part of the VPN GW which will be deleted -and $_.ResourceType -notlike "*/localnetworkgateways*" ` # Excluding Local Network Gateways because they're part of the VPN GW which will be deleted -and $_.ResourceType -notlike "*/virtualnetworkgateways*" ` # Excluding VPNGW because it depends on Public IP address resource which cannot be move. } #Convert our array to JSON to pass into the body $resourceids = $resourceids.resourceid | ConvertTo-JSON # Get our bearer token because we're already signed into Azure . .\Get-AzCachedAccessToken.ps1 #https://gallery.technet.microsoft.com/scriptcenter/Easily-obtain-AccessToken-3ba6e593 $BearerToken = ('Bearer {0}' -f (Get-AzCachedAccessToken)) $RequestHeader = @{ "Content-Type" = "application/json"; "Authorization" = "$BearerToken" } # Build the Body variable, using our $resourceids array $Body = @" { "resources": $resourceids, "targetResourceGroup": "/subscriptions/$targetsub/resourceGroups/$targetrgname" } "@ $URI = "https://management.azure.com/subscriptions/$subscriptionid/resourceGroups/$resourcegroup/validateMoveResources?api-version=2019-05-10" $response = Invoke-WebRequest -Uri $URI -Method POST -body $body -header $RequestHeader # Wait a while for the validation job to complete Sleep 30 # From the returned response, find the Location section of the string and extract it $a = $response.rawcontent $checkURI = $a.Substring($a.IndexOf('https:'), $a.IndexOf('Retry-After:')-$a.IndexOf('https:')) $CheckResponse = Invoke-WebRequest -Uri $checkURI -Method Get -header $RequestHeader |
Here is what your results might look like:
I haven’t found a good way yet to format this output, as it isn’t contained within the $CheckResponse variable. But you can see in this snippet there’s two errors I’m going to have to deal with:
- Exceeding Core Quota on my target subscription
- Azure Backup is in use on one of the VMs
Right now I’m using this script ad-hoc, and manually consuming the results for output in other ways. It would certainly be possible to take the output of results and format it nicely into a report; this would go well with iterating through all resource groups in a Subscription, to automate the validation process if you’re consistently moving a large amount of resources (i.e. a Pay-as-you-go transition to CSP subscriptions).
This is one of the best and most complete write ups of what the docs don’t cover. Nicely done.
Hi, excellent script! thanks. I’m just having the following error executing it:
+ $response = Invoke-WebRequest -Uri $URI -Method Post -Body $Body -Hea …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
Exception calling “Substring” with “2” argument(s): “Length cannot be less than zero.
Parameter name: length”
At D:\Users\v-wicru\OneDrive – Microsoft\Scripts\Validate migration resources.ps1:53 char:1
+ $checkURI = $a.Substring($a.IndexOf(‘https:’), ($a.IndexOf(‘Retry-Aft …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentOutOfRangeException
Any thoughts?
I can’t say I’ve hit that error. If you output $response.rawcontent to your console, do you get a big text string containing a “Location” descriptor?
The line that starts with $CheckURI is simply assembling a URL from that rawcontent string, in order to perform another Invoke-WebRequest against it.
If something went wrong with the original submission, that rawcontent wouldn’t have the expected contents; instead it may display an error from the first Invoke-WebRequest.
Did anybody has a more friendly way to export the results, so it could be more easy to work on the pendencies shown on the results?
The response is captured in the exception and can be formatted correctly. There were also a few syntax problems with this script but I can’t remember what they were. I know the sleep 30 isnt long enough for the restapi response. I ended up writing my own but this is a good step towards learning the process and overall a nice script which i used some part in reference. Thankyou.
Since this is the best resource I’ve come across on this matter, let me add the following. I’m parsing the error using the following script block “try {$response = Invoke-WebRequest -Uri $checkURI -Method Get -header $header } catch {$response = $_.ErrorDetails.Message}”. I’m also parsing the checkUri using the following because your example didn’t seem to work. $checkURI = ([String]$response.rawcontent -split ‘Location: ‘)[1]