From 3e07b3cf0d7820df32bdbcffb6bb1a0175bdc429 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Tue, 28 Jun 2022 18:27:36 +0200 Subject: [PATCH 1/6] Added function for later use --- .../tools/Clear-ManagementGroupDeployment.ps1 | 96 +++++++++++++++++++ utilities/tools/helper/Split-Array.ps1 | 47 +++++++++ 2 files changed, 143 insertions(+) create mode 100644 utilities/tools/Clear-ManagementGroupDeployment.ps1 create mode 100644 utilities/tools/helper/Split-Array.ps1 diff --git a/utilities/tools/Clear-ManagementGroupDeployment.ps1 b/utilities/tools/Clear-ManagementGroupDeployment.ps1 new file mode 100644 index 0000000000..9143039a8b --- /dev/null +++ b/utilities/tools/Clear-ManagementGroupDeployment.ps1 @@ -0,0 +1,96 @@ + +<# +.SYNOPSIS +Bulk delete all deployments on the given management group scope + +.DESCRIPTION +Bulk delete all deployments on the given management group scope + +.PARAMETER ManagementGroupId +Mandatory. The Resource ID of the Management Group to remove the deployments for. + +.PARAMETER DeploymentStatusToExclude +Optional. The status to exlude from removals. By default, we exclude any deployment that is in state 'running'. + +.EXAMPLE +Clear-ManagementGroupDeployment -ManagementGroupId 'MyManagementGroupId' + +Bulk remove all 'non-running' deployments from the Management Group with ID 'MyManagementGroupId' +#> +function Clear-ManagementGroupDeployment { + + + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true)] + [string] $ManagementGroupId, + + [Parameter(Mandatory = $false)] + [string] $DeploymentStatusToExclude = 'running' + ) + + # Load used functions + . (Join-Path $PSScriptRoot 'helper' 'Split-Array.ps1') + + $getInputObject = @{ + Method = 'GET' + Uri = "https://management.azure.com/providers/Microsoft.Management/managementGroups/$ManagementGroupId/providers/Microsoft.Resources/deployments/?api-version=2021-04-01" + Headers = @{ + Authorization = 'Bearer {0}' -f (Get-AzAccessToken).Token + } + } + $response = Invoke-RestMethod @getInputObject + + if (($response | Get-Member -MemberType 'NoteProperty').Name -notcontains 'value') { + throw ('Fetching deployments failed with error [{0}]' -f ($reponse | Out-String)) + } + + $relevantDeployments = $response.value | Where-Object { $_.properties.provisioningState -ne $DeploymentStatusToExclude } + + if (-not $relevantDeployments) { + Write-Verbose 'No deployments found' -Verbose + return + } + + $relevantDeploymentChunks = , (Split-Array -InputArray $relevantDeployments -SplitSize 100) + + Write-Verbose ('Triggering the removal of [{0}] deployments of management group [{1}]' -f $relevantDeployments.Count, $ManagementGroupId) + + $failedRemovals = 0 + $successfulRemovals = 0 + foreach ($deployments in $relevantDeploymentChunks) { + + $requests = $deployments | ForEach-Object { + @{ httpMethod = 'DELETE' + name = (New-Guid).Guid # $_.properties.correlationId # "73c46fa8-dffe-4b9e-9360-cd0dc3c7b4d8", + requestHeaderDetails = @{ + commandName = 'HubsExtension.Microsoft.Resources/deployments.BulkDelete.execute' + } + url = '/providers/Microsoft.Management/managementGroups/{0}/providers/Microsoft.Resources/deployments/{1}?api-version=2019-08-01' -f $ManagementGroupId, $_.name + } + } + + $removeInputObject = @{ + Method = 'POST' + Uri = 'https://management.azure.com/batch?api-version=2020-06-01' + Headers = @{ + Authorization = 'Bearer {0}' -f (Get-AzAccessToken).Token + 'Content-Type' = 'application/json' + } + Body = @{ + requests = $requests + } | ConvertTo-Json -Depth 4 + } + if ($PSCmdlet.ShouldProcess(('Removal of [{0}] deployments' -f $requests.Count), 'Request')) { + $response = Invoke-RestMethod @removeInputObject + + $failedRemovals += ($response.responses | Where-Object { $_.httpStatusCode -notlike '20*' } ).Count + $successfulRemovals += ($response.responses | Where-Object { $_.httpStatusCode -like '20*' } ).Count + } + } + + Write-Verbose 'Outcome' -Verbose + Write-Verbose '=======' -Verbose + Write-Verbose "Successful removals:`t`t$successfulRemovals" -Verbose + Write-Verbose "Un-successful removals:`t$failedRemovals" -Verbose +} diff --git a/utilities/tools/helper/Split-Array.ps1 b/utilities/tools/helper/Split-Array.ps1 new file mode 100644 index 0000000000..f886d194f0 --- /dev/null +++ b/utilities/tools/helper/Split-Array.ps1 @@ -0,0 +1,47 @@ +<# +.SYNOPSIS +Split a given array evenly into chunks of n-items + +.DESCRIPTION +Split a given array evenly into chunks of n-item + +.PARAMETER InputArray +Mandatory. The array to split + +.PARAMETER SplitSize +Mandatory. The chunk size to split into. + +.EXAMPLE +Split-Array -InputArray @('1','2,'3','4','5') -SplitSize 3 + +Split the given array @('1','2,'3','4','5') into chunks of size '3'. Will return the multi-demensional array @(@('1','2,'3'),@('4','5')) +#> +function Split-Array { + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [object[]] $InputArray, + + [Parameter(Mandatory = $true)] + [int] $SplitSize + ) + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + process { + + if ($splitSize -ge $InputArray.Count) { + return $InputArray + } else { + $res = @() + for ($Index = 0; $Index -lt $InputArray.Count; $Index += $SplitSize) { + $res += , ( $InputArray[$index..($index + $splitSize - 1)] ) + } + return $res + } + } + end { + Write-Debug ('{0} existed' -f $MyInvocation.MyCommand) + } +} From 9c7d8deacd83af89bf574664ccf6d1bfc4407bfe Mon Sep 17 00:00:00 2001 From: MrMCake Date: Mon, 4 Jul 2022 15:33:56 +0200 Subject: [PATCH 2/6] Minor refactoring --- utilities/tools/Clear-ManagementGroupDeployment.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utilities/tools/Clear-ManagementGroupDeployment.ps1 b/utilities/tools/Clear-ManagementGroupDeployment.ps1 index 9143039a8b..379e492a45 100644 --- a/utilities/tools/Clear-ManagementGroupDeployment.ps1 +++ b/utilities/tools/Clear-ManagementGroupDeployment.ps1 @@ -10,7 +10,7 @@ Bulk delete all deployments on the given management group scope Mandatory. The Resource ID of the Management Group to remove the deployments for. .PARAMETER DeploymentStatusToExclude -Optional. The status to exlude from removals. By default, we exclude any deployment that is in state 'running'. +Optional. The status to exlude from removals. Can be multiple. By default, we exclude any deployment that is in state 'running'. .EXAMPLE Clear-ManagementGroupDeployment -ManagementGroupId 'MyManagementGroupId' @@ -26,7 +26,7 @@ function Clear-ManagementGroupDeployment { [string] $ManagementGroupId, [Parameter(Mandatory = $false)] - [string] $DeploymentStatusToExclude = 'running' + [string[]] $DeploymentStatusToExclude = @('running') ) # Load used functions @@ -45,7 +45,7 @@ function Clear-ManagementGroupDeployment { throw ('Fetching deployments failed with error [{0}]' -f ($reponse | Out-String)) } - $relevantDeployments = $response.value | Where-Object { $_.properties.provisioningState -ne $DeploymentStatusToExclude } + $relevantDeployments = $response.value | Where-Object { $_.properties.provisioningState -notin $DeploymentStatusToExclude } if (-not $relevantDeployments) { Write-Verbose 'No deployments found' -Verbose @@ -62,7 +62,7 @@ function Clear-ManagementGroupDeployment { $requests = $deployments | ForEach-Object { @{ httpMethod = 'DELETE' - name = (New-Guid).Guid # $_.properties.correlationId # "73c46fa8-dffe-4b9e-9360-cd0dc3c7b4d8", + name = (New-Guid).Guid requestHeaderDetails = @{ commandName = 'HubsExtension.Microsoft.Resources/deployments.BulkDelete.execute' } From 6fe594e61016b4e40381a35972fd3bdb5c1bdf31 Mon Sep 17 00:00:00 2001 From: MrMCake Date: Mon, 4 Jul 2022 16:47:45 +0200 Subject: [PATCH 3/6] Updated conditions --- utilities/tools/Clear-ManagementGroupDeployment.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/tools/Clear-ManagementGroupDeployment.ps1 b/utilities/tools/Clear-ManagementGroupDeployment.ps1 index 379e492a45..c5a712456c 100644 --- a/utilities/tools/Clear-ManagementGroupDeployment.ps1 +++ b/utilities/tools/Clear-ManagementGroupDeployment.ps1 @@ -26,7 +26,7 @@ function Clear-ManagementGroupDeployment { [string] $ManagementGroupId, [Parameter(Mandatory = $false)] - [string[]] $DeploymentStatusToExclude = @('running') + [string[]] $DeploymentStatusToExclude = @('running', 'failed') ) # Load used functions From e8071ab44119c929af1696519be82dc76d065e9d Mon Sep 17 00:00:00 2001 From: MrMCake Date: Mon, 4 Jul 2022 16:49:37 +0200 Subject: [PATCH 4/6] Update to latest --- utilities/tools/Clear-ManagementGroupDeployment.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/utilities/tools/Clear-ManagementGroupDeployment.ps1 b/utilities/tools/Clear-ManagementGroupDeployment.ps1 index c5a712456c..387b0cf32a 100644 --- a/utilities/tools/Clear-ManagementGroupDeployment.ps1 +++ b/utilities/tools/Clear-ManagementGroupDeployment.ps1 @@ -10,12 +10,17 @@ Bulk delete all deployments on the given management group scope Mandatory. The Resource ID of the Management Group to remove the deployments for. .PARAMETER DeploymentStatusToExclude -Optional. The status to exlude from removals. Can be multiple. By default, we exclude any deployment that is in state 'running'. +Optional. The status to exlude from removals. Can be multiple. By default, we exclude any deployment that is in state 'running' or 'failed'. .EXAMPLE Clear-ManagementGroupDeployment -ManagementGroupId 'MyManagementGroupId' -Bulk remove all 'non-running' deployments from the Management Group with ID 'MyManagementGroupId' +Bulk remove all 'non-running' & 'non-failed' deployments from the Management Group with ID 'MyManagementGroupId' + +.EXAMPLE +Clear-ManagementGroupDeployment -ManagementGroupId 'MyManagementGroupId' -DeploymentStatusToExclude @('running') + +Bulk remove all 'non-failed' deployments from the Management Group with ID 'MyManagementGroupId' #> function Clear-ManagementGroupDeployment { From 309ba04b6d439880947172f695332d52e6f6bd9f Mon Sep 17 00:00:00 2001 From: Alexander Sehr Date: Mon, 4 Jul 2022 19:48:27 +0200 Subject: [PATCH 5/6] Update utilities/tools/Clear-ManagementGroupDeployment.ps1 Co-authored-by: Erika Gressi <56914614+eriqua@users.noreply.github.com> --- utilities/tools/Clear-ManagementGroupDeployment.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/tools/Clear-ManagementGroupDeployment.ps1 b/utilities/tools/Clear-ManagementGroupDeployment.ps1 index 387b0cf32a..13d92e6b06 100644 --- a/utilities/tools/Clear-ManagementGroupDeployment.ps1 +++ b/utilities/tools/Clear-ManagementGroupDeployment.ps1 @@ -20,7 +20,7 @@ Bulk remove all 'non-running' & 'non-failed' deployments from the Management Gro .EXAMPLE Clear-ManagementGroupDeployment -ManagementGroupId 'MyManagementGroupId' -DeploymentStatusToExclude @('running') -Bulk remove all 'non-failed' deployments from the Management Group with ID 'MyManagementGroupId' +Bulk remove all 'non-running' deployments from the Management Group with ID 'MyManagementGroupId' #> function Clear-ManagementGroupDeployment { From e70497e0a2ec53e06d7fd37ed56a097511958c6e Mon Sep 17 00:00:00 2001 From: MrMCake Date: Mon, 4 Jul 2022 20:21:34 +0200 Subject: [PATCH 6/6] Added docs --- ... CI environment - Deployment validation.md | 4 ++- ...gement Group Deployment removal utility.md | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 docs/wiki/The CI environment - Management Group Deployment removal utility.md diff --git a/docs/wiki/The CI environment - Deployment validation.md b/docs/wiki/The CI environment - Deployment validation.md index cd5eb638dd..0420a185b3 100644 --- a/docs/wiki/The CI environment - Deployment validation.md +++ b/docs/wiki/The CI environment - Deployment validation.md @@ -35,7 +35,9 @@ If any of these parallel deployments require multiple/different/specific resourc The parameter files used in this stage should ideally cover as many configurations as possible to validate the template flexibility, i.e., to verify that the module can cover multiple scenarios in which the given Azure resource may be used. Using the example of the CosmosDB module, we may want to have one parameter file for the minimum amount of required parameters, one parameter file for each CosmosDB type to test individual configurations, and at least one parameter file testing the supported extension resources such as RBAC & diagnostic settings. -> **Note**: Since every customer environment might be different due to applied Azure Policies or security policies, modules might behave differently and naming conventions need to be verified beforehand. +> **Note:** Since every customer environment might be different due to applied Azure Policies or security policies, modules might behave differently and naming conventions need to be verified beforehand. + +> **Note:** Management-Group deployments may eventually exceed the limit of [800](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#management-group-limits) and require you to remove some of them manually. If you are faced with any corresponding error message you can manually remove deployments on a Management-Group-Level on scale using one of our [utilities](./The%20CI%20environment%20-%20Management%20Group%20Deployment%20removal%20utility). ### Output example diff --git a/docs/wiki/The CI environment - Management Group Deployment removal utility.md b/docs/wiki/The CI environment - Management Group Deployment removal utility.md new file mode 100644 index 0000000000..531d282251 --- /dev/null +++ b/docs/wiki/The CI environment - Management Group Deployment removal utility.md @@ -0,0 +1,26 @@ +Use this script to remove Management-Group-Level Azure deployments on scale. This may be necessary in cases where you run many (test) deployments in this scope as Azure currently only auto-removes deployments from an [Resource-Group & Subscription](https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/deployment-history-deletions?tabs=azure-powershell) scope. The resulting error message may look similar to `Creating the deployment '' would exceed the quota of '800'. The current deployment count is '804'. Please delete some deployments before creating a new one, or see https://aka.ms/800LimitFix for information on managing deployment limits.` + +--- + +### _Navigation_ + +- [Location](#location) +- [How it works](#how-it-works) +- [How to use it](#how-to-use-it) + +--- +# Location + +You can find the script under [`/utilities/tools/Clear-ManagementGroupDeployment`](https://github.com/Azure/ResourceModules/blob/main/utilities/tools/Clear-ManagementGroupDeployment.ps1) + +# How it works + +1. The script fetches all current deployments from Azure. +1. By default it then filters them down to non-running & non-failing deployments (can be modified). +1. Lastly, it removes all matching deployments in chunks of 100 deployments each. + +# How to use it + +For details on how to use the function, please refer to the script's local documentation. + +> **Note:** The script must be loaded ('*dot-sourced*') before the function can be invoked.