Skip to content

Commit eb8c233

Browse files
feat: delete deployment stacks (#498)
# Pull Request ## Description Add deployment stack deletion ## License By submitting this pull request, I confirm that my contribution is made under the terms of the projects associated license.
1 parent c9d057f commit eb8c233

File tree

1 file changed

+171
-48
lines changed

1 file changed

+171
-48
lines changed

src/ALZ/Public/Remove-PlatformLandingZone.ps1

Lines changed: 171 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ function Remove-PlatformLandingZone {
9494
subscriptions. This is useful when you want to preserve deployment records for audit or compliance purposes.
9595
Default: $false (delete deployments)
9696
97+
.PARAMETER SkipDeploymentStackDeletion
98+
A switch parameter that skips deployment stack deletion operations at both the management group and subscription
99+
levels. When specified, the function will not delete deployment stacks from management groups or subscriptions.
100+
This is useful when you want to preserve deployment stacks or lack the necessary permissions to delete them.
101+
Default: $false (delete deployment stacks)
102+
97103
.PARAMETER SkipOrphanedRoleAssignmentDeletion
98104
A switch parameter that skips orphaned role assignment deletion operations at both the management group and
99105
subscription levels. When specified, the function will not delete role assignments where the principal no
@@ -122,6 +128,14 @@ function Remove-PlatformLandingZone {
122128
containing "Custom" anywhere in their name).
123129
Default: Empty array (delete all custom role definitions)
124130
131+
.PARAMETER DeploymentStacksToDeleteNamePatterns
132+
An array of wildcard patterns for deployment stack names that should be deleted. Only deployment stacks
133+
matching any of these patterns will be deleted during the deployment stack cleanup process. If the array
134+
is empty, all deployment stacks will be deleted (default behavior). Each pattern is evaluated using a
135+
-like expression with wildcards at the start and end (e.g., a pattern of "alz" will match deployment stacks
136+
containing "alz" anywhere in their name).
137+
Default: Empty array (delete all deployment stacks)
138+
125139
.EXAMPLE
126140
Remove-PlatformLandingZone -ManagementGroups @("alz-platform", "alz-landingzones")
127141
@@ -184,6 +198,12 @@ function Remove-PlatformLandingZone {
184198
Removes management groups and resource groups but skips resetting Microsoft Defender plans and deleting
185199
deployment history. Useful for faster cleanup when Defender configuration and audit trails should be preserved.
186200
201+
.EXAMPLE
202+
Remove-PlatformLandingZone -ManagementGroups @("alz-test") -SkipDeploymentStackDeletion
203+
204+
Removes management groups and resource groups but skips deleting deployment stacks. Useful when you want to
205+
preserve deployment stacks for managed resource cleanup or lack the necessary permissions to delete them.
206+
187207
.EXAMPLE
188208
Remove-PlatformLandingZone -Subscriptions @("Sub-Test-001") -SkipOrphanedRoleAssignmentDeletion
189209
@@ -208,6 +228,12 @@ function Remove-PlatformLandingZone {
208228
Removes management groups and resource groups but only deletes custom role definitions with names containing
209229
"Test-Role" or "Temporary". Useful when you want to clean up specific custom roles while preserving others.
210230
231+
.EXAMPLE
232+
Remove-PlatformLandingZone -ManagementGroups @("alz-test") -DeploymentStacksToDeleteNamePatterns @("alz-", "test-")
233+
234+
Removes management groups and resource groups but only deletes deployment stacks with names containing
235+
"alz-" or "test-". Useful when you want to clean up specific deployment stacks while preserving others.
236+
211237
.NOTES
212238
This function uses Azure CLI commands and requires:
213239
- Azure CLI to be installed and available in the system path
@@ -267,10 +293,12 @@ function Remove-PlatformLandingZone {
267293
[switch]$PlanMode,
268294
[switch]$SkipDefenderPlanReset,
269295
[switch]$SkipDeploymentDeletion,
296+
[switch]$SkipDeploymentStackDeletion,
270297
[switch]$SkipOrphanedRoleAssignmentDeletion,
271298
[switch]$SkipCustomRoleDefinitionDeletion,
272299
[string[]]$ManagementGroupsToDeleteNamePatterns = @(),
273-
[string[]]$RoleDefinitionsToDeleteNamePatterns = @()
300+
[string[]]$RoleDefinitionsToDeleteNamePatterns = @(),
301+
[string[]]$DeploymentStacksToDeleteNamePatterns = @()
274302
)
275303

276304
function Write-ToConsoleLog {
@@ -558,7 +586,10 @@ function Remove-PlatformLandingZone {
558586
[string]$ScopeId,
559587
[int]$ThrottleLimit,
560588
[switch]$PlanMode,
561-
[string]$TempLogFileForPlan
589+
[string]$TempLogFileForPlan,
590+
[switch]$SkipDeploymentStackDeletion,
591+
[switch]$SkipDeploymentDeletion,
592+
[string[]]$DeploymentStacksToDeleteNamePatterns = @()
562593
)
563594

564595
if(-not $PSCmdlet.ShouldProcess("Delete Deployments", "delete")) {
@@ -568,57 +599,143 @@ function Remove-PlatformLandingZone {
568599
$funcWriteToConsoleLog = ${function:Write-ToConsoleLog}.ToString()
569600
$isSubscriptionScope = $ScopeType -eq "subscription"
570601

571-
Write-ToConsoleLog "Checking for deployments to delete in $($ScopeType): $ScopeNameForLogs" -NoNewLine
602+
# Delete deployment stacks first (before regular deployments)
603+
if(-not $SkipDeploymentStackDeletion) {
604+
Write-ToConsoleLog "Checking for deployment stacks to delete in $($ScopeType): $ScopeNameForLogs" -NoNewLine
605+
606+
$deploymentStacks = @()
607+
if ($isSubscriptionScope) {
608+
$deploymentStacks = (az stack sub list --subscription $ScopeId --query "[].{name:name,id:id}" -o json 2>$null) | ConvertFrom-Json
609+
} else {
610+
$deploymentStacks = (az stack mg list --management-group-id $ScopeId --query "[].{name:name,id:id}" -o json 2>$null) | ConvertFrom-Json
611+
}
612+
613+
# Filter deployment stacks to only include those matching deletion patterns
614+
if ($DeploymentStacksToDeleteNamePatterns -and $DeploymentStacksToDeleteNamePatterns.Count -gt 0) {
615+
$filteredDeploymentStacks = @()
616+
foreach($stack in $deploymentStacks) {
617+
$shouldDelete = $false
618+
foreach($pattern in $DeploymentStacksToDeleteNamePatterns) {
619+
if($stack.name -like "*$pattern*") {
620+
Write-ToConsoleLog "Including deployment stack for deletion due to pattern match '$pattern': $($stack.name)" -NoNewLine
621+
$shouldDelete = $true
622+
break
623+
}
624+
}
625+
if($shouldDelete) {
626+
$filteredDeploymentStacks += $stack
627+
} else {
628+
Write-ToConsoleLog "Skipping deployment stack (no pattern match): $($stack.name)" -NoNewLine
629+
}
630+
}
631+
$deploymentStacks = $filteredDeploymentStacks
632+
}
633+
634+
if ($deploymentStacks -and $deploymentStacks.Count -gt 0) {
635+
Write-ToConsoleLog "Found $($deploymentStacks.Count) deployment stack(s) in $($ScopeType): $ScopeNameForLogs" -NoNewLine
636+
637+
$deploymentStacks | ForEach-Object -Parallel {
638+
$deploymentStack = $_
639+
$scopeId = $using:ScopeId
640+
$scopeNameForLogs = $using:ScopeNameForLogs
641+
$scopeType = $using:ScopeType
642+
$funcWriteToConsoleLog = $using:funcWriteToConsoleLog
643+
${function:Write-ToConsoleLog} = $funcWriteToConsoleLog
644+
$isSubscriptionScope = $using:isSubscriptionScope
645+
646+
Write-ToConsoleLog "Deleting deployment stack: $($deploymentStack.name) from $($scopeType): $scopeNameForLogs" -NoNewLine
647+
$result = $null
648+
if($isSubscriptionScope) {
649+
if($using:PlanMode) {
650+
Write-ToConsoleLog `
651+
"Deleting deployment stack: $($deploymentStack.name) from $($scopeType): $scopeNameForLogs", `
652+
"Would run: az stack sub delete --subscription $scopeId --name $($deploymentStack.name) --aou detachAll --yes" `
653+
-IsPlan -LogFilePath $using:TempLogFileForPlan
654+
} else {
655+
$result = az stack sub delete --subscription $scopeId --name $deploymentStack.name --aou detachAll --yes 2>&1
656+
}
657+
} else {
658+
if($using:PlanMode) {
659+
Write-ToConsoleLog `
660+
"Deleting deployment stack: $($deploymentStack.name) from $($scopeType): $scopeNameForLogs", `
661+
"Would run: az stack mg delete --management-group-id $scopeId --name $($deploymentStack.name) --aou detachAll --yes" `
662+
-IsPlan -LogFilePath $using:TempLogFileForPlan
663+
} else {
664+
$result = az stack mg delete --management-group-id $scopeId --name $deploymentStack.name --aou detachAll --yes 2>&1
665+
}
666+
}
667+
668+
if (!$result) {
669+
Write-ToConsoleLog "Deleted deployment stack: $($deploymentStack.name) from $($scopeType): $scopeNameForLogs" -NoNewLine
670+
} else {
671+
Write-ToConsoleLog "Failed to delete deployment stack: $($deploymentStack.name) from $($scopeType): $scopeNameForLogs" -IsWarning -NoNewLine
672+
}
673+
} -ThrottleLimit $ThrottleLimit
572674

573-
$deployments = @()
574-
if ($isSubscriptionScope) {
575-
$deployments = (az deployment sub list --subscription $ScopeId --query "[].name" -o json) | ConvertFrom-Json
675+
Write-ToConsoleLog "All deployment stacks processed in $($ScopeType): $ScopeNameForLogs" -NoNewLine
676+
} else {
677+
Write-ToConsoleLog "No deployment stacks found in $($ScopeType): $ScopeNameForLogs, skipping." -NoNewLine
678+
}
576679
} else {
577-
$deployments = (az deployment mg list --management-group-id $ScopeId --query "[].name" -o json) | ConvertFrom-Json
680+
Write-ToConsoleLog "Skipping deployment stack deletion in $($ScopeType): $ScopeNameForLogs" -NoNewLine
578681
}
579682

580-
if ($deployments -and $deployments.Count -gt 0) {
581-
Write-ToConsoleLog "Found $($deployments.Count) deployment(s) in $($ScopeType): $scopeNameForLogs" -NoNewLine
683+
if(-not $SkipDeploymentDeletion) {
684+
Write-ToConsoleLog "Checking for deployments to delete in $($ScopeType): $ScopeNameForLogs" -NoNewLine
582685

583-
$deployments | ForEach-Object -Parallel {
584-
$deploymentName = $_
585-
$scopeId = $using:ScopeId
586-
$scopeNameForLogs = $using:ScopeNameForLogs
587-
$funcWriteToConsoleLog = $using:funcWriteToConsoleLog
588-
${function:Write-ToConsoleLog} = $funcWriteToConsoleLog
686+
$deployments = @()
687+
if ($isSubscriptionScope) {
688+
$deployments = (az deployment sub list --subscription $ScopeId --query "[].name" -o json) | ConvertFrom-Json
689+
} else {
690+
$deployments = (az deployment mg list --management-group-id $ScopeId --query "[].name" -o json) | ConvertFrom-Json
691+
}
589692

590-
Write-ToConsoleLog "Deleting deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -NoNewLine
591-
$result = $null
592-
if($isSubscriptionScope) {
593-
if($using:PlanMode) {
594-
Write-ToConsoleLog `
595-
"Deleting deployment: $deploymentName from $($scopeType): $scopeNameForLogs", `
596-
"Would run: az deployment sub delete --subscription $scopeId --name $deploymentName" `
597-
-IsPlan -LogFilePath $using:TempLogFileForPlan
693+
if ($deployments -and $deployments.Count -gt 0) {
694+
Write-ToConsoleLog "Found $($deployments.Count) deployment(s) in $($ScopeType): $scopeNameForLogs" -NoNewLine
695+
696+
$deployments | ForEach-Object -Parallel {
697+
$deploymentName = $_
698+
$scopeId = $using:ScopeId
699+
$scopeNameForLogs = $using:ScopeNameForLogs
700+
$funcWriteToConsoleLog = $using:funcWriteToConsoleLog
701+
${function:Write-ToConsoleLog} = $funcWriteToConsoleLog
702+
$isSubscriptionScope = $using:isSubscriptionScope
703+
704+
Write-ToConsoleLog "Deleting deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -NoNewLine
705+
$result = $null
706+
if($isSubscriptionScope) {
707+
if($using:PlanMode) {
708+
Write-ToConsoleLog `
709+
"Deleting deployment: $deploymentName from $($scopeType): $scopeNameForLogs", `
710+
"Would run: az deployment sub delete --subscription $scopeId --name $deploymentName" `
711+
-IsPlan -LogFilePath $using:TempLogFileForPlan
712+
} else {
713+
$result = az deployment sub delete --subscription $scopeId --name $deploymentName 2>&1
714+
}
598715
} else {
599-
$result = az deployment sub delete --subscription $scopeId --name $deploymentName 2>&1
716+
if($using:PlanMode) {
717+
Write-ToConsoleLog `
718+
"Deleting deployment: $deploymentName from $($scopeType): $scopeNameForLogs", `
719+
"Would run: az deployment mg delete --management-group-id $scopeId --name $deploymentName" `
720+
-IsPlan -LogFilePath $using:TempLogFileForPlan
721+
} else {
722+
$result = az deployment mg delete --management-group-id $scopeId --name $deploymentName 2>&1
723+
}
600724
}
601-
} else {
602-
if($using:PlanMode) {
603-
Write-ToConsoleLog `
604-
"Deleting deployment: $deploymentName from $($scopeType): $scopeNameForLogs", `
605-
"Would run: az deployment mg delete --management-group-id $scopeId --name $deploymentName" `
606-
-IsPlan -LogFilePath $using:TempLogFileForPlan
725+
726+
if (!$result) {
727+
Write-ToConsoleLog "Deleted deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -NoNewLine
607728
} else {
608-
$result = az deployment mg delete --management-group-id $scopeId --name $deploymentName 2>&1
729+
Write-ToConsoleLog "Failed to delete deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -IsWarning -NoNewLine
609730
}
610-
}
611-
612-
if (!$result) {
613-
Write-ToConsoleLog "Deleted deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -NoNewLine
614-
} else {
615-
Write-ToConsoleLog "Failed to delete deployment: $deploymentName from $($scopeType): $scopeNameForLogs" -IsWarning -NoNewLine
616-
}
617-
} -ThrottleLimit $using:ThrottleLimit
731+
} -ThrottleLimit $ThrottleLimit
618732

619-
Write-ToConsoleLog "All deployments processed in $($scopeType): $scopeNameForLogs" -NoNewLine
733+
Write-ToConsoleLog "All deployments processed in $($scopeType): $scopeNameForLogs" -NoNewLine
734+
} else {
735+
Write-ToConsoleLog "No deployments found in $($scopeType): $scopeNameForLogs, skipping." -NoNewLine
736+
}
620737
} else {
621-
Write-ToConsoleLog "No deployments found in $($scopeType): $scopeNameForLogs, skipping." -NoNewLine
738+
Write-ToConsoleLog "Skipping deployment deletion in $($ScopeType): $ScopeNameForLogs" -NoNewLine
622739
}
623740
}
624741

@@ -961,8 +1078,8 @@ function Remove-PlatformLandingZone {
9611078
} -ThrottleLimit $ThrottleLimit
9621079
}
9631080

964-
# Delete deployments from target management groups that are not being deleted
965-
if($managementGroupsFound.Count -ne 0 -and -not $SkipDeploymentDeletion -and -not $DeleteTargetManagementGroups) {
1081+
# Delete deployments and deployment stacks from target management groups that are not being deleted
1082+
if($managementGroupsFound.Count -ne 0 -and (-not $SkipDeploymentDeletion -or -not $SkipDeploymentStackDeletion) -and -not $DeleteTargetManagementGroups) {
9661083
$managementGroupsFound | ForEach-Object -Parallel {
9671084
$managementGroupId = $_.Name
9681085
$managementGroupDisplayName = $_.DisplayName
@@ -978,11 +1095,14 @@ function Remove-PlatformLandingZone {
9781095
-ScopeId $managementGroupId `
9791096
-ThrottleLimit $using:ThrottleLimit `
9801097
-PlanMode:$using:PlanMode `
981-
-TempLogFileForPlan $using:TempLogFileForPlan
1098+
-TempLogFileForPlan $using:TempLogFileForPlan `
1099+
-SkipDeploymentStackDeletion:$using:SkipDeploymentStackDeletion `
1100+
-SkipDeploymentDeletion:$using:SkipDeploymentDeletion `
1101+
-DeploymentStacksToDeleteNamePatterns $using:DeploymentStacksToDeleteNamePatterns
9821102

9831103
} -ThrottleLimit $ThrottleLimit
9841104
} else {
985-
Write-ToConsoleLog "Skipping deployment deletion for management groups" -NoNewLine
1105+
Write-ToConsoleLog "Skipping deployment and deployment stack deletion for management groups" -NoNewLine
9861106
}
9871107

9881108
# Delete orphaned role assignments from target management groups that are not being deleted
@@ -1172,16 +1292,19 @@ function Remove-PlatformLandingZone {
11721292
Write-ToConsoleLog "Skipping Microsoft Defender for Cloud Plans reset in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine
11731293
}
11741294

1175-
if(-not $using:SkipDeploymentDeletion) {
1295+
if(-not $using:SkipDeploymentDeletion -or -not $using:SkipDeploymentStackDeletion) {
11761296
Remove-DeploymentsForScope `
11771297
-ScopeType "subscription" `
11781298
-ScopeNameForLogs "$($subscription.Name) (ID: $($subscription.Id))" `
11791299
-ScopeId $subscription.Id `
11801300
-ThrottleLimit $using:ThrottleLimit `
11811301
-PlanMode:$using:PlanMode `
1182-
-TempLogFileForPlan $using:TempLogFileForPlan
1302+
-TempLogFileForPlan $using:TempLogFileForPlan `
1303+
-SkipDeploymentStackDeletion:$using:SkipDeploymentStackDeletion `
1304+
-SkipDeploymentDeletion:$using:SkipDeploymentDeletion `
1305+
-DeploymentStacksToDeleteNamePatterns $using:DeploymentStacksToDeleteNamePatterns
11831306
} else {
1184-
Write-ToConsoleLog "Skipping subscription level deployment deletion in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine
1307+
Write-ToConsoleLog "Skipping subscription level deployment and deployment stack deletion in subscription: $($subscription.Name) (ID: $($subscription.Id))" -NoNewLine
11851308
}
11861309

11871310
if(-not $using:SkipOrphanedRoleAssignmentDeletion) {

0 commit comments

Comments
 (0)