Skip to content

Commit

Permalink
Improve delete-management-groups.bat script (Azure#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
skeeler authored Mar 30, 2022
1 parent 2e5a56b commit 453a0f8
Show file tree
Hide file tree
Showing 17 changed files with 189 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .pipelines/management-groups.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
# ----------------------------------------------------------------------------------

trigger:
trigger:
batch: true
branches:
include:
Expand Down
Binary file added docs/media/onboarding/management-groups-01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/management-groups-02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/management-groups-03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/management-groups-04.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-1-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-1-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-1-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-2-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-2-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-2-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-2-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-3-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-3-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/onboarding/run-3-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 73 additions & 3 deletions docs/onboarding/azure-devops-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Git for Windows includes Unix utilities (e.g. `cut`, `tr`, etc.) that are used b

Verify that the path to these utilities is included in the `echo %PATH%` output, i.e. it must be part of your system path or user environment path for the user running these scripts. The default installation location for these files is `C:\Program Files\Git\usr\bin`.

> NOTE: In addition to ensuring the path to these utilities is included in your `%PATH%`, you should also verify that it precedes the `C:\Windows\System32` path. This is due to a conflict with the Linux version of the `sort.exe` utility and the Windows version of the `sort.exe` utility. The following script files invoke `sort.exe` and expect the Linux version: `delete-management-groups.bat` and `list-management-groups.bat`.
> NOTE: In addition to ensuring the path to these utilities is included in your `%PATH%`, you should also verify that it precedes the `C:\Windows\System32` path. This is due to a conflict with the Linux version of the `sort.exe` utility and the Windows version of the `sort.exe` utility. The following script file invokes `sort.exe` and expects the Linux version: `list-management-groups.bat`.
---

Expand Down Expand Up @@ -315,11 +315,11 @@ The `/scripts/onboarding/.gitignore` file prevents the `./output` folder (defaul
| Area | File Name | Description
| ---- | ---- | ----
| Azure | `create-security-group.bat` | Create an Azure security group to be used in the `securityGroupObjectIds` values in environment configuration (YAML) files and subscription configuration (JSON) files
| Azure | `delete-management-groups.bat` | Deletes all management groups in the current tenant, with the exception of the 'Tenant Root Group'. It is useful for resetting the management groups in your Azure AD tenant. Exercise caution when using this script as it will remove **all** management groups in the Azure AD tenant.
| Azure | `delete-management-groups.bat` | Deletes all management groups in the current tenant, with the exception of the 'Tenant Root Group'. It is useful for resetting the management groups in your Azure AD tenant. Exercise caution when using this script as it will remove some or possibly all management groups in the Azure AD tenant, depending on how it is invoked. Review and ensure you understand the information in [Appendix A - Deleting Management Groups](#appendix-a---deleting-management-groups) before using this script.
| Azure | `list-management-groups.bat` | List all Management Groups in the current tenant. It is useful for validating the successful deployment of the Management Groups pipeline.
| Azure DevOps | `create-pipelines.bat` | Create the Azure DevOps pipelines for landing zone deployment.
| Azure DevOps | `create-service-endpoint.bat` | Create a new Azure DevOps service endpoint for use with Azure Pipelines.
| Azure DevOps | `create-variable-group.bat` | Create a variable group to store secrets used by the pipelines.
| Azure DevOps | `create-variable-group.bat` | Create a variable group and variables within that group to store secrets used by the pipelines. The default variable group name used in the example scripts is `firewall-secrets`, and may be configured via the `%DEVOPS_VARIABLES_GROUP_NAME%` environment variable. The default variable names used in the example scripts are `var-hubnetwork-nva-fwUsername` and `var-hubnetwork-nva-fwPassword`. The variable group name may be set to any valid value for an Azure DevOps pipeline variable group. The variable names must remain the same as the example scripts, otherwise the pipeline definitions will need to be updated to match different variable names.
| Azure DevOps | `delete-pipelines.bat` | Delete the Azure DevOps pipelines.
| Azure DevOps | `delete-service-endpoint.bat` | Delete the specified service endpoint used by Azure DevOps pipelines.
| Azure DevOps | `run-pipelines.bat` | Runs all landing zone pipelines in sequence.
Expand All @@ -337,3 +337,73 @@ The `/scripts/onboarding/.gitignore` file prevents the `./output` folder (defaul
| Tenant | `remove-root-user-access-admin.bat` | Remove the specified user from elevated "User Access Administrator" role at tenant root scope.
| Utility | `whereami-azure.bat` | Show all identities signed-in with the current Azure CLI session.
| Utility | `whoami-azure.bat` | Show the active identity signed-in with the current Azure CLI session.

## Appendices

### Appendix A - Deleting Management Groups

The `delete-management-groups.bat` script offers a convenient way of quickly tearing down the management group hierarchy (or a portion thereof) is an Azure AD tenant. This can be useful if you are experimenting with landing zone deployments and need a fast, programmatic way to remove management group deployments.

This script is designed to take an optional parameter that represents the topmost management group you would like to to remove, along with all child management groups in the hierarchy. If no parameter is provided, the default behavior of this script is to operate at the `Tenant Root Group`, which is the topmost management group level in an Azure AD tenant. If a parameter is provided, it must specify an existing management group (use the ID, not the Display Name).

Note that as part of the removal of management groups in the hierarchy, the `delete-management-groups.bat` script has the following additional effects:

- Any subscriptions that are contained in a removed management group will themselves be removed from that management group. This is required, otherwise the management group cannot be deleted.
- Any custom role definitions (i.e. those starting with the name `Custom - `) will be deleted.
- Any custom role assignments on subscriptions that are removed from management groups about to be deleted will themselves be deleted.

The following example illustrates the effects of invoking the `delete-management-groups.bat` script using both the parameter and parameter-less invocation methods.

We start with an example management group hierarchy (`MyOrganization`) deployed by the `CanadaPubSecALZ` pipeline, along with 2 manually created management groups (`ChildGroup1` and `ChildGroup2`) created at the tenant root management scope:

![](../media/onboarding/management-groups-01.png)

Next, we run the `delete-management-groups.bat` script, passing in as a parameter one of the nested management groups with ID `Management`:

![](../media/onboarding/run-1-1.png)

After confirming we want to proceed, the affected objects are displayed (one management group and one subscription), a warning message is displayed, and we are prompted for confirmation to proceed:

![](../media/onboarding/run-1-2.png)

After confirming we want to proceed, an existing subscription is removed from the management group, a check for custom role definitions at the management group scope is performed (none found), and the management group is removed:

![](../media/onboarding/run-1-3.png)

After the first run of the `delete-management-groups.bat` script, the updated management group hierarchy appears as follows:

![](../media/onboarding/management-groups-02.png)

Note in the above image that the subscription `ALZ-Logging` has been re-parented to the Tenant Root Group and the `Management` management group has been deleted as a result of this script run.

For the second run of the `delete-management-groups.bat` script, we will remove the entire `MyOrganization` management group hierarchy without affecting the other 2 management groups that are children of the Tenant Root Group. We do this by passing the ID of the `MyOrganization` management group as a parameter to the script:

![](../media/onboarding/run-2-1.png)

Similar to the previous script run, we are shown the current environment variables in effect and prompted to confirm (above), and are shown the affected objects along with a warning message and prompted to confirm again (below):

![](../media/onboarding/run-2-2.png)

As the script runs, it outputs each operation and results in sequence:

![](../media/onboarding/run-2-3.png)
![](../media/onboarding/run-2-4.png)

After the second time this script is run, the `MyOrganization` management group (and all child management groups) have been deleted, and the updated management group hierarchy appears as follows:

![](../media/onboarding/management-groups-03.png)

Note that the `ALZ-Networking` and `ALZ-Workload-Generalized` subscriptions are now parented by the Tenant Root Group. The `ChildGroup1` and `ChildGroup2` management groups remain intact, since they were not part of the `MyOrganization` management group hierarchy.

For the third and final run of the `delete-management-groups.bat` script, we will remove all management groups under the Tenant Root Group. We do this by invoking the script with no parameters. When invoked with no parameters, the script uses the value stored in the `%DEVOPS_TENANT_ID%` environment variable, which effectively removes all management groups under the Tenant Root Group. An equivalent way to achieve this is by passing the ID of the Tenant Root Group (your Azure AD tenant ID) as the parameter value; however, it is easier to just run the script with no parameters.

Note that we could have achieved the same effect with a single invocation of the script with no parameters the first time we ran it. The previous invocations are simply examples of how to remove and individual management group or portion of the complete management group hierarchy.

![](../media/onboarding/run-3-1.png)
![](../media/onboarding/run-3-2.png)
![](../media/onboarding/run-3-3.png)

After the third time this script is run (with no parameters), only the Tenant Root Group remains, and all subscriptions have been re-parented under it:

![](../media/onboarding/management-groups-04.png)

161 changes: 115 additions & 46 deletions scripts/onboarding/delete-management-groups.bat
Original file line number Diff line number Diff line change
Expand Up @@ -8,72 +8,141 @@ REM // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WAR
REM // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
REM // ----------------------------------------------------------------------------------

set TMPFILE=management-groups.txt
REM Need delayed expansion to process !var! environment variables
setlocal EnableDelayedExpansion

REM Get currently signed-in user identity
echo.
echo Getting currently signed-in user identity...
echo.
call az ad signed-in-user show --query "userPrincipalName"
REM Environment variables local to this script
set MGMT_GROUP_HIERARCHY_FILE=management-groups.txt
set MGMT_GROUP_ROOT_ID=%DEVOPS_TENANT_ID%
if not "%1" == "" set MGMT_GROUP_ROOT_ID=%1

REM Get default subscription information
echo.
echo Getting default subscription information...
echo This script will remove all management groups below level: [%MGMT_GROUP_ROOT_ID%].
echo If the specific level is not the Tenant Root Group, it will be removed also.
echo.
call az account list --query "[?isDefault].{Name:name, Id:id, AAD:homeTenantId, User:user.name}" -o table

REM Get list of management groups in reverse order
echo DEVOPS_OUTPUT_DIR = %DEVOPS_OUTPUT_DIR%
echo DEVOPS_TENANT_ID = %DEVOPS_TENANT_ID%
echo.
echo Target Management Group = %MGMT_GROUP_ROOT_ID%
echo.
echo Getting list of all management groups...
echo If these settings are not correct, please exit, update/run the set-variables.[YourEnv].bat script, and re-run this script
echo.
call az account management-group list -o tsv | sed "/Tenant Root Group/d" | cut -f 1 - | sort -k 1r - >%TMPFILE%
choice /C YN /M "Do you want to proceed?"
if errorlevel 2 exit /b 0

REM Check output directory exists
if not exist %DEVOPS_OUTPUT_DIR% (
echo Intermediate output directory does not exist. Creating it at [%DEVOPS_OUTPUT_DIR%]
md %DEVOPS_OUTPUT_DIR%
)

REM Show user all management groups found
REM Get the current management group hierarchy
echo.
echo Management groups
echo -----------------
cat %TMPFILE%
echo Retrieving management group hierarchy, starting at [%MGMT_GROUP_ROOT_ID%], and storing in: %DEVOPS_OUTPUT_DIR%\%MGMT_GROUP_HIERARCHY_FILE%
call az account management-group show --only-show-errors --name %MGMT_GROUP_ROOT_ID% -e -r | jq -r "recurse | [.id] + [.name] + [.displayName] + [.type | match(\"^^.*/(managementGroups^|subscriptions)$\"; \"g\").captures[0].string] + ( .children?[]? | [.id] + [.name] + [.displayName] + [.type | match(\"^^.*/(managementGroups^|subscriptions)$\"; \"g\").captures[0].string] ) | @tsv" >%DEVOPS_OUTPUT_DIR%\%MGMT_GROUP_HIERARCHY_FILE%

REM Check that some results were returned, i.e. the management group is valid
for %%R in (%DEVOPS_OUTPUT_DIR%\%MGMT_GROUP_HIERARCHY_FILE%) do (
if %%~zR EQU 0 (
echo Unable to locate management group [%MGMT_GROUP_ROOT_ID%]
echo Exiting script
exit /b 1
)
)

echo.
echo ------------ -----------
echo Parent Group Child Group
echo ------------ -----------
cat %DEVOPS_OUTPUT_DIR%\%MGMT_GROUP_HIERARCHY_FILE% | cut -f 2,6
echo ------------ -----------

REM Prompt user confirmation to delete all management groups
echo.
echo WARNING:
echo -------------------------------------------------------------------
echo Continuing this script will delete the listed management groups,
echo which will also disassociate any subscriptions associated with each
echo management group. Subscriptions associated with a management group
echo that is being deleted will be re-parented to the tenant root scope.
echo -------------------------------------------------------------------------------
echo Continuing this script will delete these listed management groups, and will
echo also disassociate any subscriptions associated with each management group.
echo Subscriptions associated with a management group that is being deleted will
echo be re-parented to the tenant root scope. Any custom role definitions that
echo have names starting with "Custom - " may also be affected. Any custom role
echo assignments at an included subscription scope will be removed, and any
echo custom role definitions scoped to an included management group will be deleted.
echo.
echo Also note that this script will delete **all** management groups
echo defined in the current tenant, whether or not they were created for
echo your `CanadaPubSecALZ` work or by some other means.
echo.
echo Be sure you understand the implications of continuing this script
echo before proceeding. If you're not 100% certain, then select "N" at
echo the following prompt.
echo -------------------------------------------------------------------
echo Ensure you understand the implications of using this script before proceeding.
echo If you're not 100%% certain, then select "N" at the following prompt.
echo -------------------------------------------------------------------------------
echo.
choice /C YN /M "Do you want to proceed?"
if errorlevel 2 exit /b 0
echo.

REM Delete all management groups (in hierarchy reverse order)
for /f usebackq %%m in (`cat %TMPFILE%`) do (
REM Process the management group hierarchy (in reverse order)
echo Processing management group hierarchy, starting at management group node [%MGMT_GROUP_ROOT_ID%]

REM Note: The 'delims=' contains a TAB character (<Alt> <Numpad: 0 0 9>). If you or your text editor convert this TAB to a SPACE, this script will no longer function as expected. Also, pay attention to DOS escape characters documented here: https://www.robvanderwoude.com/escapechars.php
for /f "usebackq tokens=1-8 delims= " %%A in (`cat %DEVOPS_OUTPUT_DIR%\%MGMT_GROUP_HIERARCHY_FILE% ^| tac`) do (
REM Capture parent element attributes
set P_ID=%%A
set P_NAME=%%B
set P_DISPLAY=%%C
set P_TYPE=%%D
REM Capture child element attributes
set C_ID=%%E
set C_NAME=%%F
set C_DISPLAY=%%G
set C_TYPE=%%H

REM Check for subscriptions that need to be removed from management group
echo Checking management group [%%m] for subscriptions that need to be removed first...
for /f "usebackq delims=" %%s in (
`call az account management-group show --name "%%m" --expand --query "children[?type=='/subscriptions'].{Name:displayName}" -o tsv`
) do (
echo removing subscription [%%s] from management group [%%m]...
call az account management-group subscription remove --name "%%m" --subscription "%%s"
if "!C_TYPE!" == "subscriptions" (
if "!P_NAME!" NEQ "%DEVOPS_TENANT_ID%" (
REM Remove custom role assignments from subscription
for /f "usebackq" %%R in (`call az role assignment list --only-show-errors --all --subscription "!C_NAME!" --query "[? contains(roleDefinitionName, 'Custom - ')]" ^| jq -r ".[].id"`) do (
echo Removing custom role assignment from subscription [!C_DISPLAY!] [!C_NAME!] role id: [%%R]
call az role assignment delete --only-show-errors --ids "%%R"
)
REM Remove subscription from management group
echo Removing subscription [!C_DISPLAY!] [!C_NAME!] from management group [!P_DISPLAY!] [!P_NAME!]
call az account management-group subscription remove --only-show-errors --name "!P_NAME!" --subscription "!C_DISPLAY!"
) else (
echo Subscription [!C_DISPLAY!] [!C_NAME!] is already parented by Tenant Root Group. No further action required.
)
) else (
if "!C_TYPE!" == "managementGroups" (
REM Delete custom role definitions at management group scope
for /f "usebackq delims=" %%L in (`call az role definition list --only-show-errors --custom-role-only true --scope "!C_ID!" --query "[? contains(assignableScopes, '!C_ID!')]" ^| jq -r ".[].roleName"`) do (
echo Deleting custom role definition [%%L] at management group [!C_DISPLAY!] [!C_NAME!] scope
call az role definition delete --only-show-errors --custom-role-only true --scope "!C_ID!" --name "%%L"
)
REM Delete management group
echo Deleting management group [!C_DISPLAY!] [!C_NAME!]
call az account management-group delete --only-show-errors --name "!C_NAME!"
) else (
echo.
echo ***ERROR*** in 'az account management-group show' output
echo Unable to find '/managementGroups' or '/subscriptions' resource type for element with identifier [!C_ID!]
echo Check intermediate output file: %DEVOPS_OUTPUT_DIR%\%MGMT_GROUP_HIERARCHY_FILE%
echo.
)
echo Deleting management group: %%m
call az account management-group delete --name %%m
)
)

REM Remove %TMPFILE% temporary file
if exist %TMPFILE% (
echo Deleting temporary file '%TMPFILE%'
erase %TMPFILE%
REM If the top level management group is not the Tenant Root Group,
REM then perform the same management group erasure steps at that level.
if "%MGMT_GROUP_ROOT_ID%" NEQ "%DEVOPS_TENANT_ID%" (
REM Capture element attributes
echo Retrieving details for management group node [%MGMT_GROUP_ROOT_ID%]
REM Note: The 'delims=' contains a TAB character (<Alt> <Numpad: 0 0 9>). If you or your text editor convert this TAB to a SPACE, this script will no longer function as expected. Also, pay attention to DOS escape characters documented here: https://www.robvanderwoude.com/escapechars.php
for /f "usebackq tokens=1-3 delims= " %%A in (`call az account management-group show --only-show-errors --name %MGMT_GROUP_ROOT_ID% ^| jq -r "[.id]+[.name]+[.displayName] | @tsv"`) do (
set C_ID=%%A
set C_NAME=%%B
set C_DISPLAY=%%C
)
REM Delete custom role definitions at management group scope
echo Deleting custom role definitions, if any, at management group [!C_DISPLAY!] [!C_NAME!] scope:
for /f "usebackq delims=" %%L in (`call az role definition list --only-show-errors --custom-role-only true --scope "!C_ID!" --query "[? contains(assignableScopes, '!C_ID!')]" ^| jq -r ".[].roleName"`) do (
echo Deleting custom role definition [%%L]
call az role definition delete --only-show-errors --custom-role-only true --scope "!C_ID!" --name "%%L"
)
REM Delete management group
echo Deleting management group [!C_DISPLAY!] [!C_NAME!]
call az account management-group delete --only-show-errors --name "!C_NAME!"
)

0 comments on commit 453a0f8

Please sign in to comment.