diff --git a/.vscode/tours/E2Etesttour.tour b/.vscode/tours/E2Etesttour.tour index ffd19b0fa..4505fae7c 100644 --- a/.vscode/tours/E2Etesttour.tour +++ b/.vscode/tours/E2Etesttour.tour @@ -333,18 +333,101 @@ }, { "file": "tests/pipelines/bicep-build-to-validate.yml", - "description": "Takes output variable from previous job to later determine if anything was deployed. ", - "line": 251 + "selection": { + "start": { + "line": 248, + "character": 1 + }, + "end": { + "line": 249, + "character": 1 + } + }, + "description": "Start ARM WhatIf checks to confirm no false positives from whats just been deployed" + }, + { + "file": "tests/pipelines/bicep-build-to-validate.yml", + "selection": { + "start": { + "line": 252, + "character": 1 + }, + "end": { + "line": 253, + "character": 1 + } + }, + "description": "Only runs if Management Groups were deployed, using same condition" + }, + { + "file": "tests/pipelines/bicep-build-to-validate.yml", + "selection": { + "start": { + "line": 256, + "character": 1 + }, + "end": { + "line": 257, + "character": 1 + } + }, + "description": "Run WhatIf deployment and only report on changes, if any." + }, + { + "file": "tests/pipelines/bicep-build-to-validate.yml", + "selection": { + "start": { + "line": 259, + "character": 11 + }, + "end": { + "line": 261, + "character": 17 + } + }, + "description": "If there are any changes fail the step and report as output to Azure DevOps Pipeline" }, { "file": "tests/pipelines/bicep-build-to-validate.yml", - "description": "Run cleanup if anything was deployed.", - "line": 258 + "selection": { + "start": { + "line": 267, + "character": 1 + }, + "end": { + "line": 268, + "character": 1 + } + }, + "description": "Takes output variable from previous job to later determine if anything was deployed. " }, { "file": "tests/pipelines/bicep-build-to-validate.yml", - "description": "Run PowerShell script to do the following (in order):\r\n- Move subscription from connectivity management group to tenant root group.\r\n- Delete all resource groups in subscription\r\n- Remove all subscription scope deployments\r\n- Remove all tenant scope deployments\r\n- Remove management group structure\r\n", - "line": 262 + "selection": { + "start": { + "line": 274, + "character": 1 + }, + "end": { + "line": 275, + "character": 1 + } + }, + "description": "Run cleanup if anything was deployed." + }, + { + "file": "tests/pipelines/bicep-build-to-validate.yml", + "selection": { + "start": { + "line": 275, + "character": 5 + }, + "end": { + "line": 281, + "character": 17 + } + }, + "description": "Run PowerShell script to do the following (in order):\r\n- Move subscription from connectivity management group to tenant root group.\r\n- Delete all resource groups in subscription\r\n- Remove all subscription scope deployments\r\n- Remove all tenant scope deployments\r\n- Remove management group structure" } ] } \ No newline at end of file diff --git a/infra-as-code/bicep/modules/managementGroups/README.md b/infra-as-code/bicep/modules/managementGroups/README.md index 97f0ec15e..3305e0076 100644 --- a/infra-as-code/bicep/modules/managementGroups/README.md +++ b/infra-as-code/bicep/modules/managementGroups/README.md @@ -18,38 +18,98 @@ The Management Groups module deploys a management group hierarchy in a customer' The module requires the following inputs: -| Parameter | Type | Description | Requirements | Example | -| ------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------------------- | -| parTopLevelManagementGroupPrefix | string | Prefix for the management group hierarchy. This management group will be created as part of the deployment. | 2-10 characters | `alz` | -| parTopLevelManagementGroupDisplayName | string | Display name for top level management group. This name will be applied to the management group prefix defined in `parTopLevelManagementGroupPrefix` parameter. | Minimum two characters | `Azure Landing Zones` | -| parTelemetryOptOut | bool | Set Parameter to true to Opt-out of deployment telemetry | Mandatory input, default: `false` | `false` | +| Parameter | Type | Description | Requirements | Example | +| ------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | --------------------------------------------------------------------------------------- | +| parTopLevelManagementGroupPrefix | string | Prefix for the management group hierarchy. This management group will be created as part of the deployment. | 2-10 characters | `alz` | +| parTopLevelManagementGroupDisplayName | string | Display name for top level management group. This name will be applied to the management group prefix defined in `parTopLevelManagementGroupPrefix` parameter. | Minimum two characters | `Azure Landing Zones` | +| parTopLevelManagementGroupParentId | string | Optional parent for Management Group hierarchy, used as intermediate root Management Group parent, if specified. If empty, default, will deploy beneath Tenant Root Management Group. | Not required input, default `''` | `/providers/Microsoft.Management/managementGroups/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | +| parLandingZoneMgAlzDefaultsEnable | bool | Deploys Corp & Online Management Groups beneath Landing Zones Management Group if set to true. | Mandatory input, default: `true` | `true` | +| parLandingZoneMgConfidentialEnable | bool | Deploys Confidential Corp & Confidential Online Management Groups beneath Landing Zones Management Group if set to true. | Mandatory input, default: `false` | `false` | +| parLandingZoneMgChildren | object | Dictionary Object to allow additional or different child Management Groups of Landing Zones Management Group to be deployed. | Not required input, default `{}` | `{pci: {displayName: 'PCI'}}` | +| parTelemetryOptOut | bool | Set Parameter to true to Opt-out of deployment telemetry | Mandatory input, default: `false` | `false` | + +### Child Landing Zone Management Groups Flexibility + +This module allows some flexibility for deploying child Landing Zone Management Groups, e.g. Management Groups that live beneath the Landing Zones Management Group. This flexibility is controlled by three parameters which are detailed below. All of these parameters can be used together to tailor the child Landing Zone Management Groups. + +- `parLandingZoneMgAlzDefaultsEnable` + - Boolean - defaults to `true` + - **Required** + - Deploys following child Landing Zone Management groups if set to `true`: + - `Corp` + - `Online` + - *These are the default ALZ Management Groups as per the conceptual architecture* +- `parLandingZoneMgConfidentialEnable` + - Boolean - defaults to `false` + - **Required** + - Deploys following child Landing Zone Management groups if set to `true`: + - `Confidential Corp` + - `Confidential Online` +- `parLandingZoneMgChildren` + - Object - default is an empty object `{}` + - **Optional** + - Deploys whatever you specify in the object as child Landing Zone Management groups. + +These three parameters are then used to collate a single variable that is used to create the child Landing Zone Management Groups. Duplicates are removed if entered. This is done by using the `union()` function in bicep. + +> Investigate the variable called `varLandingZoneMgChildrenUnioned` if you want to see how this works in the module. + +#### `parLandingZoneMgChildren` Input Examples + +Below are some examples of how to use this input parameter in both Bicep & JSON formats. + +##### Bicep Example + +```bicep +parLandingZoneMgChildren: { + pci: { + displayName: 'PCI' + } + 'another-example': { + displayName: 'Another Example' + } +} +``` + +##### JSON Parameter File Input Example + +```json +"parLandingZoneMgChildren": { + "value": { + "pci": { + "displayName": "PCI" + }, + "another-example": { + "displayName": "Another Example" + } + } +} +``` ## Outputs The module will generate the following outputs: -| Output | Type | Example | -| ------------------------------------------ | ------ | -------------------------------------------------------------------------- | -| outTopLevelManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz | -| outPlatformManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-platform | -| outPlatformManagementManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-platform-management | -| outPlatformConnectivityManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-platform-connectivity | -| outPlatformIdentityManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-platform-identity | -| outLandingZonesManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-landingzones | -| outLandingZonesCorpManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-landingzones-corp | -| outLandingZonesOnlineManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-landingzones-online | -| outSandboxManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-sandbox | -| outDecommissionedManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-decommissioned | -| outTopLevelManagementGroupName | string | alz | -| outPlatformManagementGroupName | string | alz-platform | -| outPlatformManagementManagementGroupName | string | alz-platform-management | -| outPlatformConnectivityManagementGroupName | string | alz-platform-connectivity | -| outPlatformIdentityManagementGroupName | string | alz-platform-identity | -| outLandingZonesManagementGroupName | string | alz-landingzones | -| outLandingZonesCorpManagementGroupName | string | alz-landingzones-corp | -| outLandingZonesOnlineManagementGroupName | string | alz-landingzones-online | -| outSandboxManagementGroupName | string | alz-sandbox | -| outDecommissionedManagementGroupName | string | alz-decommissioned | +| Output | Type | Example | +| ------------------------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| outTopLevelManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz | +| outPlatformManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-platform | +| outPlatformManagementManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-platform-management | +| outPlatformConnectivityManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-platform-connectivity | +| outPlatformIdentityManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-platform-identity | +| outLandingZonesManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-landingzones | +| outLandingZoneChildrenManagementGroupIds | array | `[/providers/Microsoft.Management/managementGroups/alz-landingzones-corp, /providers/Microsoft.Management/managementGroups/alz-landingzones-online]` | +| outSandboxManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-sandbox | +| outDecommissionedManagementGroupId | string | /providers/Microsoft.Management/managementGroups/alz-decommissioned | +| outTopLevelManagementGroupName | string | alz | +| outPlatformManagementGroupName | string | alz-platform | +| outPlatformManagementManagementGroupName | string | alz-platform-management | +| outPlatformConnectivityManagementGroupName | string | alz-platform-connectivity | +| outPlatformIdentityManagementGroupName | string | alz-platform-identity | +| outLandingZonesManagementGroupName | string | alz-landingzones | +| outLandingZoneChildrenManagementGroupNames | array | `[Corp, Online]` | +| outSandboxManagementGroupName | string | alz-sandbox | +| outDecommissionedManagementGroupName | string | alz-decommissioned | ## Deployment diff --git a/infra-as-code/bicep/modules/managementGroups/managementGroups.bicep b/infra-as-code/bicep/modules/managementGroups/managementGroups.bicep index 4a2c72bda..67f9019f6 100644 --- a/infra-as-code/bicep/modules/managementGroups/managementGroups.bicep +++ b/infra-as-code/bicep/modules/managementGroups/managementGroups.bicep @@ -9,6 +9,18 @@ param parTopLevelManagementGroupPrefix string = 'alz' @minLength(2) param parTopLevelManagementGroupDisplayName string = 'Azure Landing Zones' +@description('Optional parent for Management Group hierarchy, used as intermediate root Management Group parent, if specified. If empty, default, will deploy beneath Tenant Root Management Group.') +param parTopLevelManagementGroupParentId string = '' + +@description('Deploys Corp & Online Management Groups beneath Landing Zones Management Group if set to true.') +param parLandingZoneMgAlzDefaultsEnable bool = true + +@description('Deploys Confidential Corp & Confidential Online Management Groups beneath Landing Zones Management Group if set to true.') +param parLandingZoneMgConfidentialEnable bool = false + +@description('Dictionary Object to allow additional or different child Management Groups of Landing Zones Management Group to be deployed.') +param parLandingZoneMgChildren object = {} + @description('Set Parameter to true to Opt-out of deployment telemetry') param parTelemetryOptOut bool = false @@ -39,16 +51,30 @@ var varLandingZoneMg = { displayName: 'Landing Zones' } -var varLandingZoneCorpMg = { - name: '${parTopLevelManagementGroupPrefix}-landingzones-corp' - displayName: 'Corp' +// Used if parLandingZoneMgAlzDefaultsEnable == true +var varLandingZoneMgChildrenAlzDefault = { + corp: { + displayName: 'Corp' + } + online: { + displayName: 'Online' + } } -var varLandingZoneOnlineMg = { - name: '${parTopLevelManagementGroupPrefix}-landingzones-online' - displayName: 'Online' +// Used if parLandingZoneMgConfidentialEnable == true +var varLandingZoneMgChildrenConfidential = { + 'confidential-corp': { + displayName: 'Confidential Corp' + } + 'confidential-online': { + displayName: 'Confidential Online' + } } +// Build final onject based on input parameters for child MGs of LZs +var varLandingZoneMgChildrenUnioned = (parLandingZoneMgAlzDefaultsEnable && parLandingZoneMgConfidentialEnable && (!empty(parLandingZoneMgChildren))) ? union(varLandingZoneMgChildrenAlzDefault, varLandingZoneMgChildrenConfidential, parLandingZoneMgChildren) : (parLandingZoneMgAlzDefaultsEnable && parLandingZoneMgConfidentialEnable && (empty(parLandingZoneMgChildren))) ? union(varLandingZoneMgChildrenAlzDefault, varLandingZoneMgChildrenConfidential) : (parLandingZoneMgAlzDefaultsEnable && !parLandingZoneMgConfidentialEnable && (!empty(parLandingZoneMgChildren))) ? union(varLandingZoneMgChildrenAlzDefault, parLandingZoneMgChildren) : (parLandingZoneMgAlzDefaultsEnable && !parLandingZoneMgConfidentialEnable && (empty(parLandingZoneMgChildren))) ? varLandingZoneMgChildrenAlzDefault : (!parLandingZoneMgAlzDefaultsEnable && parLandingZoneMgConfidentialEnable && (!empty(parLandingZoneMgChildren))) ? union(varLandingZoneMgChildrenConfidential, parLandingZoneMgChildren) : (!parLandingZoneMgAlzDefaultsEnable && parLandingZoneMgConfidentialEnable && (empty(parLandingZoneMgChildren))) ? varLandingZoneMgChildrenConfidential : (!parLandingZoneMgAlzDefaultsEnable && !parLandingZoneMgConfidentialEnable && (!empty(parLandingZoneMgChildren))) ? parLandingZoneMgChildren : (!parLandingZoneMgAlzDefaultsEnable && !parLandingZoneMgConfidentialEnable && (empty(parLandingZoneMgChildren))) ? {} : {} + + // Sandbox Management Group var varSandboxMg = { name: '${parTopLevelManagementGroupPrefix}-sandbox' @@ -69,6 +95,11 @@ resource resTopLevelMg 'Microsoft.Management/managementGroups@2021-04-01' = { name: parTopLevelManagementGroupPrefix properties: { displayName: parTopLevelManagementGroupDisplayName + details: { + parent: { + id: (empty(parTopLevelManagementGroupParentId) ? '/providers/Microsoft.Management/managementGroups/${tenant().tenantId}' : parTopLevelManagementGroupParentId) + } + } } } @@ -159,29 +190,18 @@ resource resPlatformIdentityMg 'Microsoft.Management/managementGroups@2021-04-01 } // Level 3 - Child Management Groups under Landing Zones MG -resource resLandingZonesCorpMg 'Microsoft.Management/managementGroups@2021-04-01' = { - name: varLandingZoneCorpMg.name - properties: { - displayName: varLandingZoneCorpMg.displayName - details: { - parent: { - id: resLandingZonesMg.id - } - } - } -} -resource resLandingZonesOnlineMg 'Microsoft.Management/managementGroups@2021-04-01' = { - name: varLandingZoneOnlineMg.name +resource resLandingZonesChildMgs 'Microsoft.Management/managementGroups@2021-04-01' = [for mg in items(varLandingZoneMgChildrenUnioned): if (!empty(varLandingZoneMgChildrenUnioned)) { + name: '${parTopLevelManagementGroupPrefix}-landingzones-${mg.key}' properties: { - displayName: varLandingZoneOnlineMg.displayName + displayName: mg.value.displayName details: { parent: { id: resLandingZonesMg.id } } } -} +}] // Optional Deployment for Customer Usage Attribution module modCustomerUsageAttribution '../../CRML/customerUsageAttribution/cuaIdTenant.bicep' = if (!parTelemetryOptOut) { @@ -199,8 +219,7 @@ output outPlatformConnectivityManagementGroupId string = resPlatformConnectivity output outPlatformIdentityManagementGroupId string = resPlatformIdentityMg.id output outLandingZonesManagementGroupId string = resLandingZonesMg.id -output outLandingZonesCorpManagementGroupId string = resLandingZonesCorpMg.id -output outLandingZonesOnlineManagementGroupId string = resLandingZonesOnlineMg.id +output outLandingZoneChildrenMangementGroupIds array = [for mg in items(varLandingZoneMgChildrenUnioned): '/providers/Microsoft.Management/managementGroups/${parTopLevelManagementGroupPrefix}-landingzones-${mg.key}' ] output outSandboxManagementGroupId string = resSandboxMg.id @@ -215,8 +234,7 @@ output outPlatformConnectivityManagementGroupName string = resPlatformConnectivi output outPlatformIdentityManagementGroupName string = resPlatformIdentityMg.name output outLandingZonesManagementGroupName string = resLandingZonesMg.name -output outLandingZonesCorpManagementGroupName string = resLandingZonesCorpMg.name -output outLandingZonesOnlineManagementGroupName string = resLandingZonesOnlineMg.name +output outLandingZoneChildrenMangementGroupNames array = [for mg in items(varLandingZoneMgChildrenUnioned): mg.value.displayName ] output outSandboxManagementGroupName string = resSandboxMg.name diff --git a/infra-as-code/bicep/modules/managementGroups/parameters/managementGroups.parameters.all.json b/infra-as-code/bicep/modules/managementGroups/parameters/managementGroups.parameters.all.json index d9a53bd46..7216f1b8f 100644 --- a/infra-as-code/bicep/modules/managementGroups/parameters/managementGroups.parameters.all.json +++ b/infra-as-code/bicep/modules/managementGroups/parameters/managementGroups.parameters.all.json @@ -8,6 +8,18 @@ "parTopLevelManagementGroupDisplayName": { "value": "Azure Landing Zones" }, + "parTopLevelManagementGroupParentId": { + "value": "" + }, + "parLandingZoneMgAlzDefaultsEnable": { + "value": true + }, + "parLandingZoneMgConfidentialEnable": { + "value": false + }, + "parLandingZoneMgChildren": { + "value": {} + }, "parTelemetryOptOut": { "value": false } diff --git a/tests/pipelines/bicep-build-to-validate.yml b/tests/pipelines/bicep-build-to-validate.yml index 082653b2a..f409baa88 100644 --- a/tests/pipelines/bicep-build-to-validate.yml +++ b/tests/pipelines/bicep-build-to-validate.yml @@ -245,6 +245,22 @@ jobs: script: | az deployment group create --resource-group $(ResourceGroupName) --template-file infra-as-code/bicep/modules/vnetPeering/vnetPeering.bicep --parameters @infra-as-code/bicep/modules/vnetPeering/parameters/vnetPeering.parameters.min.json parDestinationVirtualNetworkId="/subscriptions/$(subscriptionId)/resourceGroups/$(ResourceGroupName)/providers/Microsoft.Network/virtualNetworks/vnet-spoke" parSourceVirtualNetworkName="alz-hub-eastus" parDestinationVirtualNetworkName="vnet-spoke" + # Verify that WhatIf does not find differences between code and environment thats just been deployed + - task: Bash@3 + displayName: Az CLI After Deployment What-If Management Groups for PR + name: whatif_mgs + condition: and(or(ne(variables['gitManagementOUTPUT'], ''), ne(variables['gitLoggingOUTPUT'], ''), ne(variables['gitSpokeOUTPUT'], ''), ne(variables['gitHubOUTPUT'], ''), ne(variables['gitVwanOUTPUT'], ''), ne(variables['gitVwanNwcOUTPUT'], ''), ne(variables['gitVnetPeerOUTPUT'], '')), ne(variables['subscriptionId'], '')) + inputs: + targetType: 'inline' + script: | + result=$(az deployment tenant what-if --template-file infra-as-code/bicep/modules/managementGroups/managementGroups.bicep --parameters @infra-as-code/bicep/modules/managementGroups/parameters/managementGroups.parameters.min.json parTopLevelManagementGroupPrefix=$(ManagementGroupPrefix) --location $(Location) --exclude-change-types Ignore NoChange --only-show-errors) + if [[ $result != *'Resource changes: no change.'* ]] + then + echo "##vso[task.logissue type=error]WhatIf reports difference between code and environment thats just been deployed" + echo "$result" + exit 1 + fi + - job: bicep_cleanup dependsOn: bicep_deploy variables: