Created
March 4, 2026 04:23
-
-
Save MauricioZa/2743c669d889a59f4b839acba5b2f692 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Azure VM Restore Runbook - Full VM restore from Recovery Services Vault | |
| # For use in Azure Automation Account | |
| # Uses Managed Identity for authentication | |
| param( | |
| [Parameter(Mandatory=$true)] | |
| [string]$SubscriptionId, | |
| [Parameter(Mandatory=$true)] | |
| [string]$VaultName, | |
| [Parameter(Mandatory=$true)] | |
| [string]$VMName, | |
| [Parameter(Mandatory=$true)] | |
| [string]$restoredVmName, | |
| [Parameter(Mandatory=$true)] | |
| [string]$TargetResourceGroupName, | |
| [Parameter(Mandatory=$true)] | |
| [string]$StagingStorageAccountResourceGroup, | |
| [Parameter(Mandatory=$true)] | |
| [string]$StagingStorageAccountName, | |
| [Parameter(Mandatory=$false)] | |
| [int]$RecoveryPointIndex = 0, | |
| [Parameter(Mandatory=$false)] | |
| [int]$MaxDaysBack = 60, | |
| [Parameter(Mandatory=$false)] | |
| [int]$RestoreTimeoutMinutes = 720 | |
| ) | |
| # Connect to Azure using Managed Identity | |
| try { | |
| Write-Output "Connecting to Azure using Managed Identity..." | |
| Connect-AzAccount -Identity -ErrorAction Stop | |
| Write-Output "Successfully connected to Azure" | |
| } catch { | |
| Write-Error "Failed to connect to Azure using Managed Identity: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # Set the subscription context | |
| try { | |
| Write-Output "Setting subscription context to: $SubscriptionId" | |
| Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop | |
| $context = Get-AzContext | |
| Write-Output "Successfully set context to subscription: $($context.Subscription.Name)" | |
| } catch { | |
| Write-Error "Failed to set subscription context: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # region Get Recovery Services Vault and VM details | |
| # Get the Recovery Services vault | |
| try { | |
| Write-Output "Retrieving Recovery Services vault: $VaultName" | |
| $vault = Get-AzRecoveryServicesVault -Name $VaultName -ErrorAction Stop | |
| Write-Output "Found Recovery Services vault: $($vault.Name) (Resource Group: $($vault.ResourceGroupName))" | |
| Set-AzRecoveryServicesVaultContext -Vault $vault | |
| Write-Output "Set vault context successfully" | |
| } catch { | |
| Write-Error "Recovery Services vault '$VaultName' not found: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # Get the backup item for the specified VM | |
| try { | |
| Write-Output "Searching for backup item for VM: $VMName" | |
| $namedContainer = Get-AzRecoveryServicesBackupContainer -ContainerType "AzureVM" -FriendlyName $VMName -VaultId $vault.ID -ErrorAction Stop | |
| $backupitem = Get-AzRecoveryServicesBackupItem -Container $namedContainer -WorkloadType "AzureVM" -VaultId $vault.ID -ErrorAction Stop | |
| if (!$backupItem) { | |
| throw "No backup data found for VM '$VMName'" | |
| } | |
| Write-Output "Backup item found for VM: $($backupItem.Name)" | |
| } catch { | |
| Write-Error "Failed to find backup item for VM '$VMName': $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # Get recovery points | |
| try { | |
| Write-Output "Retrieving recovery points for the last $MaxDaysBack days..." | |
| $startDate = (Get-Date).AddDays(-$MaxDaysBack).ToUniversalTime() | |
| $endDate = (Get-Date).ToUniversalTime() | |
| $rpList = Get-AzRecoveryServicesBackupRecoveryPoint -Item $backupItem -StartDate $startDate -EndDate $endDate -VaultId $vault.ID | |
| if (!$rpList -or $rpList.Count -eq 0) { | |
| throw "No recovery points found for VM '$VMName' in the selected date range" | |
| } | |
| Write-Output "Found $($rpList.Count) recovery points for VM '$VMName'" | |
| for ($i = 0; $i -lt [Math]::Min($rpList.Count, 10); $i++) { | |
| $rp = $rpList[$i] | |
| $timeUTC = [DateTime]$rp.RecoveryPointTime | |
| $timeStr = $timeUTC.ToString("yyyy-MM-dd HH:mm:ss 'UTC'") | |
| Write-Output "[$i] $timeStr ($($rp.RecoveryPointType))" | |
| } | |
| # Validate recovery point index | |
| if ($RecoveryPointIndex -ge $rpList.Count -or $RecoveryPointIndex -lt 0) { | |
| Write-Error "Invalid RecoveryPointIndex: $RecoveryPointIndex. Valid range: 0-$($rpList.Count - 1)" | |
| exit 1 | |
| } | |
| $chosenRP = $rpList[$RecoveryPointIndex] | |
| Write-Output "Selected recovery point: Time: $($chosenRP.RecoveryPointTime), Type: $($chosenRP.RecoveryPointType)" | |
| } catch { | |
| Write-Error "Failed to retrieve recovery points: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # Validate target resource group | |
| try { | |
| Write-Output "Validating target resource group: $TargetResourceGroupName" | |
| $targetRG = Get-AzResourceGroup -Name $TargetResourceGroupName -ErrorAction Stop | |
| Write-Output "Target resource group validated: $($targetRG.ResourceGroupName)" | |
| } catch { | |
| Write-Error "Target resource group '$TargetResourceGroupName' not found: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # Validate staging storage account | |
| try { | |
| Write-Output "Validating staging storage account: $StagingStorageAccountName in RG: $StagingStorageAccountResourceGroup" | |
| $storageAcct = Get-AzStorageAccount -ResourceGroupName $StagingStorageAccountResourceGroup -Name $StagingStorageAccountName -ErrorAction Stop | |
| Write-Output "Staging storage account validated: $($storageAcct.StorageAccountName)" | |
| } catch { | |
| Write-Error "Storage account '$StagingStorageAccountName' not found in resource group '$StagingStorageAccountResourceGroup': $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # Handle tag policy exemptions | |
| try { | |
| Write-Output "Checking for 'Require a tag on resources' policy..." | |
| $policyScope = $targetRG.ResourceId | |
| $tagPolicyAssignments = Get-AzPolicyAssignment -Scope $policyScope | Where-Object { $_.properties.DisplayName -like "*Require a tag on resources*"} | |
| if ($tagPolicyAssignments) { | |
| foreach ($assignment in $tagPolicyAssignments) { | |
| $exemptionName = "TempTagExemption-$($assignment.Name)-$(Get-Date -Format 'yyyyMMddHHmmss')" | |
| $expiresOn = (Get-Date).AddHours(4) | |
| try { | |
| $assignmentScope = $assignment.properties.Scope | |
| New-AzPolicyExemption -Name $exemptionName -PolicyAssignment $assignment -Scope $assignmentScope -ExemptionCategory "Waiver" -ExpiresOn $expiresOn -ErrorAction SilentlyContinue | Out-Null | |
| Write-Output "Created 4-hour exemption for policy: $($assignment.Properties.DisplayName)" | |
| } catch { | |
| Write-Warning "Failed to create policy exemption: $($_.Exception.Message)" | |
| } | |
| } | |
| } else { | |
| Write-Output "No 'Require a tag on resources' policy assignments found" | |
| } | |
| } catch { | |
| Write-Warning "Error checking policy assignments: $($_.Exception.Message)" | |
| } | |
| #endregion | |
| # region Restore Process | |
| # Initiate the restore job | |
| try { | |
| Write-Output "Initiating restore job (this may take several minutes)..." | |
| $restoreJob = Restore-AzRecoveryServicesBackupItem -RecoveryPoint $chosenRP -StorageAccountName $StagingStorageAccountName -StorageAccountResourceGroupName $StagingStorageAccountResourceGroup -TargetResourceGroupName $targetRG.ResourceGroupName -VaultId $vault.ID -UseSystemAssignedIdentity -ErrorAction Stop | |
| if ($restoreJob) { | |
| Write-Output "Restore job initiated successfully!" | |
| Write-Output "Job ID: $($restoreJob.JobId)" | |
| Write-Output "Status: $($restoreJob.Status)" | |
| } else { | |
| throw "Failed to initiate restore job - no job object returned" | |
| } | |
| } catch { | |
| Write-Error "Failed to initiate restore job: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # Wait for the restore job to complete | |
| try { | |
| Write-Output "Waiting for restore job to complete (timeout: $RestoreTimeoutMinutes minutes)..." | |
| Wait-AzRecoveryServicesBackupJob -Job $restoreJob -Timeout ($RestoreTimeoutMinutes * 60) | |
| $restorejob = Get-AzRecoveryServicesBackupJob -Job $restorejob -VaultId $vault.ID | |
| $details = Get-AzRecoveryServicesBackupJobDetail -Job $restorejob -VaultId $vault.ID | |
| Write-Output "-------------------------" | |
| Write-Output "RESTORE JOB DETAILS" | |
| Write-Output "-------------------------" | |
| Write-Output "Job ID: $($restorejob.JobId)" | |
| Write-Output "Activity ID: $($restorejob.ActivityId)" | |
| Write-Output "Job Type: $($restorejob.JobType)" | |
| Write-Output "Operation: $($restorejob.Operation)" | |
| Write-Output "Status: $($restorejob.Status)" | |
| Write-Output "WorkloadType: $($restorejob.WorkloadType)" | |
| Write-Output "Duration: $($restorejob.Duration)" | |
| Write-Output "Start Time: $($restorejob.StartTime)" | |
| Write-Output "End Time: $($restorejob.EndTime)" | |
| if ($restorejob.ErrorDetails) { | |
| Write-Output "Error Details:" | |
| Write-Output "Error Code: $($restorejob.ErrorDetails.ErrorCode)" | |
| Write-Output "Error Message: $($restorejob.ErrorDetails.ErrorMessage)" | |
| Write-Output "Recommendations: $($restorejob.ErrorDetails.Recommendations)" | |
| } | |
| if ($details -and $details.SubTasks) { | |
| Write-Output "Sub Tasks:" | |
| $details.SubTasks | ForEach-Object { | |
| Write-Output " - Task: $($_.Name)" | |
| Write-Output " Status: $($_.Status)" | |
| Write-Output " Start Time: $($_.StartTime)" | |
| Write-Output " End Time: $($_.EndTime)" | |
| } | |
| } | |
| # Check final status | |
| if ($restorejob.Status -eq "Failed") { | |
| Write-Error "Restore job failed: $($restorejob.ErrorDetails.ErrorMessage)" | |
| exit 1 | |
| } elseif ($restorejob.Status -eq "Completed") { | |
| Write-Output "Restore job completed successfully" | |
| } else { | |
| Write-Warning "Restore job status: $($restorejob.Status)" | |
| } | |
| } catch { | |
| Write-Error "Error during restore job execution: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| #endregion | |
| # region VM Deployment | |
| # Create VM from restored disks | |
| try { | |
| Write-Output "Starting VM deployment from restored disks..." | |
| $properties = $details.properties | |
| $storageAccountName = $properties["Target Storage Account Name"] | |
| $containerName = $properties["Config Blob Container Name"] | |
| $templateBlobURI = $properties["Create VM Template Blob Uri"] | |
| # Extract template name from the URI | |
| $templateName = $templateBlobURI.Split('/')[-1] | |
| Write-Output "Template name: $templateName" | |
| # Set storage account context and create SAS token | |
| Set-AzCurrentStorageAccount -Name $storageAccountName -ResourceGroupName $StagingStorageAccountResourceGroup | |
| $templateBlobFullURI = New-AzStorageBlobSASToken -Container $containerName -Blob $templateName -Permission r -FullUri | |
| # Create unique deployment name | |
| $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" | |
| $deploymentName = "VM-Restore-$VMName-$timestamp" | |
| Write-Output "Deployment name: $deploymentName" | |
| # Deploy the VM | |
| Write-Output "Starting VM deployment..." | |
| $deployment = New-AzResourceGroupDeployment -Name $deploymentName -ResourceGroupName $TargetResourceGroupName -VirtualMachineName $restoredVmName -TemplateUri $templateBlobFullURI -ErrorAction Stop | |
| } catch { | |
| Write-Error "Failed to initiate VM deployment: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| # Monitor deployment results | |
| try { | |
| Write-Output "=================================" | |
| Write-Output "VM DEPLOYMENT RESULTS" | |
| Write-Output "=================================" | |
| if ($deployment) { | |
| $deploymentDetails = Get-AzResourceGroupDeployment -ResourceGroupName $TargetResourceGroupName -Name $deploymentName | |
| Write-Output "Deployment Name: $($deploymentDetails.DeploymentName)" | |
| Write-Output "Resource Group: $($deploymentDetails.ResourceGroupName)" | |
| Write-Output "Timestamp: $($deploymentDetails.Timestamp)" | |
| Write-Output "Duration: $($deploymentDetails.Duration)" | |
| Write-Output "Deployment Status: $($deploymentDetails.ProvisioningState)" | |
| if ($deploymentDetails.ProvisioningState -eq "Succeeded") { | |
| Write-Output "VM deployment completed successfully!" | |
| } else { | |
| Write-Error "VM deployment failed with status: $($deploymentDetails.ProvisioningState)" | |
| # Show error details | |
| $operations = Get-AzResourceGroupDeploymentOperation -ResourceGroupName $TargetResourceGroupName -DeploymentName $deploymentName | |
| $operations | Where-Object { $_.ProvisioningState -eq "Failed" } | ForEach-Object { | |
| Write-Output "Failed Resource: $($_.Properties.TargetResource.ResourceName)" | |
| Write-Output "Error Code: $($_.Properties.StatusMessage.Error.Code)" | |
| Write-Output "Error Message: $($_.Properties.StatusMessage.Error.Message)" | |
| } | |
| exit 1 | |
| } | |
| # Show deployment outputs | |
| if ($deploymentDetails.Outputs) { | |
| Write-Output "Deployment Outputs:" | |
| $deploymentDetails.Outputs.PSObject.Properties | ForEach-Object { | |
| Write-Output "$($_.Name): $($_.Value.Value)" | |
| } | |
| } | |
| } else { | |
| Write-Error "Failed to initiate VM deployment - no deployment object returned" | |
| exit 1 | |
| } | |
| } catch { | |
| Write-Error "Error during VM deployment monitoring: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| #endregion | |
| Write-Output "=================================" | |
| Write-Output "RUNBOOK EXECUTION COMPLETED" | |
| Write-Output "=================================" | |
| Write-Output "VM '$VMName' has been successfully restored to resource group '$TargetResourceGroupName'" | |
| Write-Output "Restore job ID: $($restorejob.JobId)" | |
| Write-Output "Deployment name: $deploymentName" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment