scripts/PowerShell/Scripts/Remove-Spoke.ps1 (182 lines of code) (raw):
<#
.SYNOPSIS
Deletes all Azure resources for the specified research spoke.
.PARAMETER TemplateParameterFile
The path to the template parameter file, in bicepparam format, that was used to create the spoke to be deleted.
.PARAMETER TargetSubscriptionId
The subscription ID where the spoke was created.
.PARAMETER CloudEnvironment
The Azure environment where the spoke was created. Default is 'AzureCloud'.
.PARAMETER Tenant
The Azure tenant ID where the spoke was created. Default is the current tenant.
.PARAMETER Force
DANGER: Forces the deletion of the spoke resources without prompting for confirmation.
.EXAMPLE
PS> ./deploy.ps1 -TemplateParameterFile '.\main.hub.bicepparam' -TargetSubscriptionId '00000000-0000-0000-0000-000000000000'
.EXAMPLE
PS> ./deploy.ps1 '.\main.hub.bicepparam' '00000000-0000-0000-0000-000000000000'
.EXAMPLE
PS> ./deploy.ps1 '.\main.hub.bicepparam' '00000000-0000-0000-0000-000000000000' 'AzureUSGovernment'
#>
#Requires -Modules Az.Resources, Az.RecoveryServices, Az.Network, Az.DataFactory
#Requires -PSEdition Core
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory, Position = 0)]
[string]$TemplateParameterFile,
[Parameter(Mandatory, Position = 1)]
[string]$TargetSubscriptionId,
[Parameter(Position = 2)]
[string]$CloudEnvironment = 'AzureCloud',
[Parameter(Position = 3)]
[string]$Tenant = (Get-AzContext).Tenant.Id,
[Parameter()]
[switch]$Force
)
$ErrorActionPreference = 'Stop'
################################################################################
# PREPARE VARIABLES
################################################################################
# Process the template parameter file and read relevant values for use here
Write-Verbose "Using template parameter file '$TemplateParameterFile'"
[string]$TemplateParameterJsonFile = [System.IO.Path]::ChangeExtension($TemplateParameterFile, 'json')
bicep build-params $TemplateParameterFile --outfile $TemplateParameterJsonFile
# Read the values from the parameters file, to use when generating the $DeploymentName value
$ParameterFileContents = (Get-Content $TemplateParameterJsonFile | ConvertFrom-Json)
[string]$WorkloadName = $ParameterFileContents.parameters.workloadName.value
[string]$Location = $ParameterFileContents.parameters.location.value
[int]$Sequence = $ParameterFileContents.parameters.sequence.value
[string]$Environment = $ParameterFileContents.parameters.environment.value
[string]$NamingConvention = $ParameterFileContents.parameters.namingConvention.value
[string]$HubVirtualNetworkId = $ParameterFileContents.parameters.hubVNetResourceId.value
# Taken from research-spoke/main.bicep
[string]$SequenceFormat = "00"
# This here for DRY
# Replaces the placeholders {workloadName}, {location}, {env}, and {loc} with the actual values from the parameter file
[string]$IntermediateResourceNamePattern = $NamingConvention.Replace("{workloadName}", $WorkloadName).Replace("{location}", $Location).Replace("{env}", $Environment).Replace("{loc}", $Location)
# Replace the {seq} placeholder by the formatted sequence number and the placeholder for Azure Backup's sequence
# Replace the resource type placeholder by the resource group type and subtype for backup
[string]$BackupResourceGroupNamePattern = $IntermediateResourceNamePattern.Replace("{seq}", "$($Sequence.ToString($SequenceFormat))-*").Replace("{rtype}", "rg-backup").Replace("-{subWorkloadName}", "")
# Replace the {seq} placeholder, keeping the {subWorkloadName} placeholder
[string]$ResourceNamePatternSubWorkload = $IntermediateResourceNamePattern.Replace("{seq}", $Sequence.ToString($SequenceFormat))
# Remove the {subWorkloadName} placeholder
[string]$ResourceNamePattern = $ResourceNamePatternSubWorkload.Replace("-{subWorkloadName}", "")
# Create a wildcard pattern for resource group names (resource type is "rg")
[string]$ResourceGroupNamePattern = $ResourceNamePattern.Replace("{rtype}", "rg-*")
Write-Verbose "Looking for resource groups matching pattern '$ResourceGroupNamePattern'."
try {
################################################################################
# SET AZURE CONTEXT AND CHECK RESOURCE EXISTENCE
################################################################################
# Import the Azure subscription management module
Import-Module ..\Modules\AzSubscriptionManagement.psm1
$OriginalContext = Get-AzContext
# Determine if a cloud context switch is required
$AzContext = Set-AzContextWrapper -SubscriptionId $TargetSubscriptionId -Environment $CloudEnvironment -Tenant $Tenant
# Check if any resource groups exist that match the pattern
$ResourceGroups = Get-AzResourceGroup -Name $ResourceGroupNamePattern
# Get a list of Azure Backup resource groups used for holding restore collections
$BackupResourceGroups = Get-AzResourceGroup -Name $BackupResourceGroupNamePattern
if ($ResourceGroups.Count -eq 0) {
Write-Warning "No resource groups found matching pattern '$ResourceGroupNamePattern' in subscription '$((Get-AzContext).Subscription.Name)'."
exit
}
$Msg1 = "Found $($ResourceGroups.Count) resource groups matching pattern '$ResourceGroupNamePattern' in subscription '$((Get-AzContext).Subscription.Name)'.`nFound $($BackupResourceGroups.Count) Azure Backup resource groups matching pattern '$BackupResourceGroupNamePattern'."
$Msg = "$Msg1`nAny resource locks will be deleted.`nThese actions cannot be undone and data loss might occur. Do you want to continue removing this spoke?"
if (-not ($WhatIfPreference -or $Force -or $PSCmdlet.ShouldContinue($Msg, 'Confirm Spoke Removal'))) {
exit
}
# If -WhatIf is used, output the number of resource groups found
if ($WhatIfPreference) {
Write-Host $Msg1
}
if ($Force) {
Write-Verbose "Force switch specified. Proceeding with deletion of resources."
}
################################################################################
# REMOVE ANY RESOURCE LOCKS
################################################################################
Write-Host "`n1️⃣: Removing resource locks..."
$ResourceGroups | ForEach-Object {
Get-AzResourceLock -ResourceGroupName $_.ResourceGroupName | Remove-AzResourceLock -Force | Out-Null
}
################################################################################
# REMOVE THE RECOVERY SERVICES VAULT
################################################################################
# Check if the expected Recovery Services Vault exists in the expected resource group
[string]$BackupResourceGroupName = $ResourceGroupNamePattern.Replace("*", "backup")
[string]$RecoveryServicesVaultName = $ResourceNamePattern.Replace("{rtype}", "rsv")
$Vault = Get-AzRecoveryServicesVault -ResourceGroupName $BackupResourceGroupName -Name $RecoveryServicesVaultName -ErrorAction SilentlyContinue
if ($Vault) {
Write-Host "`n2️⃣: Removing Recovery Services Vault '$RecoveryServicesVaultName' in resource group '$BackupResourceGroupName'..."
& ./Recovery/Remove-rsv.ps1 -VaultName $RecoveryServicesVaultName `
-ResourceGroup $BackupResourceGroupName -SubscriptionId $TargetSubscriptionId `
-WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
}
################################################################################
# REMOVE THE DATA FACTORY MANAGED PRIVATE ENDPOINTS
################################################################################
[string]$StorageResourceGroupName = $ResourceGroupNamePattern.Replace('*', 'storage')
[string]$DataFactoryName = $ResourceNamePatternSubWorkload.Replace('{rtype}', 'adf').Replace('{subWorkloadName}', 'airlock')
# Check if the expected Data Factory exists in the expected resource group
$Factory = Get-AzDataFactoryV2 -ResourceGroupName $StorageResourceGroupName -Name $DataFactoryName -ErrorAction SilentlyContinue
if ($Factory) {
Write-Host "`n3️⃣: Removing managed private endpoints from Data Factory '$DataFactoryName' in resource group '$StorageResourceGroupName'..."
& ./DataFactory/Remove-ManagedPrivateEndpoints.ps1 -DataFactoryName $DataFactoryName `
-ResourceGroup $StorageResourceGroupName -SubscriptionId $TargetSubscriptionId `
-WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
}
################################################################################
# REMOVE THE RESOURCE GROUPS
################################################################################
Write-Host "`n4️⃣: Removing resource groups..."
# Two separate commands needed because -AsJob does not support specifying a variable
if ($PSCmdlet.ShouldProcess("spoke resource groups", "Remove")) {
$Jobs = @()
$Jobs += $ResourceGroups | Remove-AzResourceGroup -AsJob -Force -Verbose:$VerbosePreference
# Remove any Azure Backup resource groups used for holding restore collections
$Jobs += $BackupResourceGroups | Remove-AzResourceGroup -AsJob -Force -Verbose:$VerbosePreference
Write-Host "Waiting for $($Jobs.Count) resource groups to be deleted..."
$Jobs | Get-Job | Wait-Job | Select-Object -Property Id, StatusMessage, Name | Format-Table -AutoSize
}
else {
$ResourceGroups | Remove-AzResourceGroup -WhatIf | Out-Null
$BackupResourceGroups | Remove-AzResourceGroup -WhatIf | Out-Null
}
################################################################################
# REMOVE THE DISCONNECTED PEERING FROM THE RESEARCH HUB VIRTUAL NETWORK
################################################################################
[string]$VNetResourceIDPattern = "/subscriptions/(?<subscriptionId>[^/]+)/resourceGroups/(?<resourceGroupName>[^/]+)/providers/Microsoft.Network/virtualNetworks/(?<resourceName>[^/]+)"
# If there is a valid hub virtual network resource ID specified (there should be)
if ($HubVirtualNetworkId -match $VNetResourceIDPattern) {
[string]$HubSubscriptionId = $Matches['subscriptionId']
[string]$HubResourceGroupName = $Matches['resourceGroupName']
[string]$HubVirtualNetworkName = $Matches['resourceName']
# We could get the virtual network ID from the spoke resources, but it's possible that the virtual network was already deleted but the peering wasn't
[string]$SpokeVirtualNetworkName = $ResourceNamePattern.Replace("{rtype}", "vnet")
[string]$NetworkResourceGroupName = $ResourceGroupNamePattern.Replace('*', 'network')
[string]$SpokeVirtualNetworkResourceId = "/subscriptions/$TargetSubscriptionId/resourceGroups/$NetworkResourceGroupName/providers/Microsoft.Network/virtualNetworks/$SpokeVirtualNetworkName"
Write-Host "`n5️⃣: Checking disconnected peering to spoke network '$SpokeVirtualNetworkName' from hub virtual network '$HubVirtualNetworkName' in resource group '$HubResourceGroupName' in subscription '$HubSubscriptionId'..."
$AzContext = Set-AzContextWrapper -SubscriptionId $HubSubscriptionId -Environment $CloudEnvironment -Tenant $Tenant
# Remove peering explicitly from hub
Get-AzVirtualNetworkPeering -ResourceGroupName $HubResourceGroupName -VirtualNetworkName $HubVirtualNetworkName | `
# Find the peering using the peering state (to confirm it's disconnected) and the remote virtual network ID
Where-Object { $_.PeeringState -eq 'Disconnected' -and $_.RemoteVirtualNetwork.Id -eq $SpokeVirtualNetworkResourceId } | `
Remove-AzVirtualNetworkPeering -Force -Verbose:$VerbosePreference
}
else {
Write-Warning "The value found in the parameter file for 'hubVNetResourceId' ('$HubVirtualNetworkId') is not a valid Azure virtual network resource ID."
}
Write-Host "`n🔥 Script completed successfully!"
}
catch {
Write-Host "`n❌ An error occurred: $($_)"
Write-Host $_.ScriptStackTrace
Write-Host "In context $AzContext"
}
finally {
Write-Verbose "Setting Azure context back to the original subscription..."
$AzContext = Set-AzContextWrapper -SubscriptionId $OriginalContext.Subscription.Id -Environment $OriginalContext.Environment.Name -Tenant $OriginalContext.Tenant.Id
# Remove the module from the session
Remove-Module AzSubscriptionManagement -WhatIf:$false
}