diff --git a/.pipelines/management-groups.yml b/.pipelines/management-groups.yml index 55154266..005f2b33 100644 --- a/.pipelines/management-groups.yml +++ b/.pipelines/management-groups.yml @@ -7,7 +7,7 @@ # OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. # ---------------------------------------------------------------------------------- -trigger: +trigger: batch: true branches: include: diff --git a/docs/media/onboarding/management-groups-01.png b/docs/media/onboarding/management-groups-01.png new file mode 100644 index 00000000..4b738981 Binary files /dev/null and b/docs/media/onboarding/management-groups-01.png differ diff --git a/docs/media/onboarding/management-groups-02.png b/docs/media/onboarding/management-groups-02.png new file mode 100644 index 00000000..631899cf Binary files /dev/null and b/docs/media/onboarding/management-groups-02.png differ diff --git a/docs/media/onboarding/management-groups-03.png b/docs/media/onboarding/management-groups-03.png new file mode 100644 index 00000000..2e4c5e0c Binary files /dev/null and b/docs/media/onboarding/management-groups-03.png differ diff --git a/docs/media/onboarding/management-groups-04.png b/docs/media/onboarding/management-groups-04.png new file mode 100644 index 00000000..2ac1918b Binary files /dev/null and b/docs/media/onboarding/management-groups-04.png differ diff --git a/docs/media/onboarding/run-1-1.png b/docs/media/onboarding/run-1-1.png new file mode 100644 index 00000000..ea5da678 Binary files /dev/null and b/docs/media/onboarding/run-1-1.png differ diff --git a/docs/media/onboarding/run-1-2.png b/docs/media/onboarding/run-1-2.png new file mode 100644 index 00000000..5b499b08 Binary files /dev/null and b/docs/media/onboarding/run-1-2.png differ diff --git a/docs/media/onboarding/run-1-3.png b/docs/media/onboarding/run-1-3.png new file mode 100644 index 00000000..c4669bf9 Binary files /dev/null and b/docs/media/onboarding/run-1-3.png differ diff --git a/docs/media/onboarding/run-2-1.png b/docs/media/onboarding/run-2-1.png new file mode 100644 index 00000000..16cb0972 Binary files /dev/null and b/docs/media/onboarding/run-2-1.png differ diff --git a/docs/media/onboarding/run-2-2.png b/docs/media/onboarding/run-2-2.png new file mode 100644 index 00000000..0f9fe127 Binary files /dev/null and b/docs/media/onboarding/run-2-2.png differ diff --git a/docs/media/onboarding/run-2-3.png b/docs/media/onboarding/run-2-3.png new file mode 100644 index 00000000..62576842 Binary files /dev/null and b/docs/media/onboarding/run-2-3.png differ diff --git a/docs/media/onboarding/run-2-4.png b/docs/media/onboarding/run-2-4.png new file mode 100644 index 00000000..3b6229f1 Binary files /dev/null and b/docs/media/onboarding/run-2-4.png differ diff --git a/docs/media/onboarding/run-3-1.png b/docs/media/onboarding/run-3-1.png new file mode 100644 index 00000000..f0adb90c Binary files /dev/null and b/docs/media/onboarding/run-3-1.png differ diff --git a/docs/media/onboarding/run-3-2.png b/docs/media/onboarding/run-3-2.png new file mode 100644 index 00000000..bbe18dac Binary files /dev/null and b/docs/media/onboarding/run-3-2.png differ diff --git a/docs/media/onboarding/run-3-3.png b/docs/media/onboarding/run-3-3.png new file mode 100644 index 00000000..8c2c5f34 Binary files /dev/null and b/docs/media/onboarding/run-3-3.png differ diff --git a/docs/onboarding/azure-devops-scripts.md b/docs/onboarding/azure-devops-scripts.md index 8d0d2316..d9f5ed73 100644 --- a/docs/onboarding/azure-devops-scripts.md +++ b/docs/onboarding/azure-devops-scripts.md @@ -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`. --- @@ -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. @@ -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) + diff --git a/scripts/onboarding/delete-management-groups.bat b/scripts/onboarding/delete-management-groups.bat index cf45cb79..69c93d31 100644 --- a/scripts/onboarding/delete-management-groups.bat +++ b/scripts/onboarding/delete-management-groups.bat @@ -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 ( ). 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 ( ). 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!" )