From d4c318f925507aaf6488d6ea96ea2cd53328e6c1 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Thu, 15 Jan 2026 12:59:31 +0000 Subject: [PATCH 01/42] feat: accelerator permission flattening --- alz/azuredevops/main.tf | 6 +- alz/azuredevops/variables.tf | 220 ++++++------------ alz/github/main.tf | 6 +- alz/github/variables.tf | 220 ++++++------------ alz/local/main.tf | 6 +- alz/local/variables.tf | 220 ++++++------------ modules/azure/management_group.tf | 34 +++ modules/azure/role_assignments.tf | 28 ++- modules/azure/role_definitions.tf | 2 +- modules/azure/variables.tf | 34 ++- ...cals.intermediate_root_management_group.tf | 27 +++ modules/file_manipulation/outputs.tf | 17 +- modules/file_manipulation/variables.tf | 12 +- 13 files changed, 355 insertions(+), 477 deletions(-) create mode 100644 modules/azure/management_group.tf create mode 100644 modules/file_manipulation/locals.intermediate_root_management_group.tf diff --git a/alz/azuredevops/main.tf b/alz/azuredevops/main.tf index 85db8b33..707212dc 100644 --- a/alz/azuredevops/main.tf +++ b/alz/azuredevops/main.tf @@ -59,7 +59,7 @@ module "azure" { container_registry_dockerfile_name = var.agent_container_image_dockerfile container_registry_dockerfile_repository_folder_url = local.agent_container_instance_dockerfile_url custom_role_definitions = var.iac_type == "terraform" ? local.custom_role_definitions_terraform : (var.iac_type == "bicep" ? local.custom_role_definitions_bicep : local.custom_role_definitions_bicep_classic) - role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : var.role_assignments_bicep + role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : (var.iac_type == "bicep" ? var.role_assignments_bicep : var.role_assignments_bicep_classic) storage_account_blob_soft_delete_enabled = var.storage_account_blob_soft_delete_enabled storage_account_blob_soft_delete_retention_days = var.storage_account_blob_soft_delete_retention_days storage_account_blob_versioning_enabled = var.storage_account_blob_versioning_enabled @@ -67,6 +67,9 @@ module "azure" { storage_account_container_soft_delete_retention_days = var.storage_account_container_soft_delete_retention_days tenant_role_assignment_enabled = var.iac_type == "bicep" && var.bicep_tenant_role_assignment_enabled tenant_role_assignment_role_definition_name = var.bicep_tenant_role_assignment_role_definition_name + intermediate_root_management_group_creation_enabled = var.iac_type != "bicep-classic" + intermediate_root_management_group_id = module.file_manipulation.intermediate_root_management_group_id + intermediate_root_management_group_display_name = module.file_manipulation.intermediate_root_management_group_display_name } module "azure_devops" { @@ -120,4 +123,5 @@ module "file_manipulation" { agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration pipeline_files_directory_path = local.pipeline_files_directory_path pipeline_template_files_directory_path = local.pipeline_template_files_directory_path + terraform_architecture_file_path = var.terraform_architecture_file_path } \ No newline at end of file diff --git a/alz/azuredevops/variables.tf b/alz/azuredevops/variables.tf index 09f3aabc..394947c5 100644 --- a/alz/azuredevops/variables.tf +++ b/alz/azuredevops/variables.tf @@ -593,11 +593,7 @@ variable "custom_role_definitions_terraform" { - `actions` (list(string)) - Allowed Azure actions - `not_actions` (list(string)) - Denied Azure actions - Default includes 4 predefined roles: - - `alz_management_group_contributor` - Manage management group hierarchy and governance - - `alz_management_group_reader` - Read management group structure and validate deployments - - `alz_subscription_owner` - Full access to platform subscriptions - - `alz_subscription_reader` - Read/write access for platform subscription resources + Default is empty, meaning no custom roles are created. See default value for complete role action definitions. EOT @@ -609,89 +605,7 @@ variable "custom_role_definitions_terraform" { not_actions = list(string) }) })) - default = { - alz_management_group_contributor = { - name = "Azure Landing Zones Management Group Contributor ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing the Management Group Structure." - permissions = { - actions = [ - "Microsoft.Management/managementGroups/delete", - "Microsoft.Management/managementGroups/read", - "Microsoft.Management/managementGroups/subscriptions/delete", - "Microsoft.Management/managementGroups/subscriptions/write", - "Microsoft.Management/managementGroups/settings/read", - "Microsoft.Management/managementGroups/settings/write", - "Microsoft.Management/managementGroups/settings/delete", - "Microsoft.Management/managementGroups/write", - "Microsoft.Management/managementGroups/subscriptions/read", - "Microsoft.Authorization/policyDefinitions/write", - "Microsoft.Authorization/policySetDefinitions/write", - "Microsoft.Authorization/policyAssignments/write", - "Microsoft.Authorization/roleDefinitions/write", - "Microsoft.Authorization/*/read", - "Microsoft.Authorization/roleAssignments/write", - "Microsoft.Authorization/roleAssignments/delete", - "Microsoft.Insights/diagnosticSettings/write" - ] - not_actions = [] - } - } - alz_management_group_reader = { - name = "Azure Landing Zones Management Group Reader ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Reading the Management Group Structure." - permissions = { - actions = [ - "Microsoft.Management/managementGroups/read", - "Microsoft.Management/managementGroups/subscriptions/read", - "Microsoft.Management/managementGroups/settings/read", - "Microsoft.Authorization/*/read", - "Microsoft.Authorization/policyDefinitions/write", - "Microsoft.Authorization/policySetDefinitions/write", - "Microsoft.Authorization/roleDefinitions/write", - "Microsoft.Authorization/policyAssignments/write", - "Microsoft.Insights/diagnosticSettings/write", - "Microsoft.Insights/diagnosticSettings/read", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/write", - "Microsoft.Resources/deploymentStacks/read", - "Microsoft.Resources/deploymentStacks/validate/action" - ] - not_actions = [] - } - } - alz_subscription_owner = { - name = "Azure Landing Zones Subscription Owner ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing in platform subscriptions." - permissions = { - actions = [ - "*" - ] - not_actions = [] - } - } - alz_subscription_reader = { - name = "Azure Landing Zones Subscription Reader ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Reading the platform subscriptions." - permissions = { - actions = [ - "*/read", - "Microsoft.Resources/subscriptions/resourceGroups/write", - "Microsoft.ManagedIdentity/userAssignedIdentities/write", - "Microsoft.Automation/automationAccounts/write", - "Microsoft.OperationalInsights/workspaces/write", - "Microsoft.OperationalInsights/workspaces/linkedServices/write", - "Microsoft.OperationsManagement/solutions/write", - "Microsoft.Insights/dataCollectionRules/write", - "Microsoft.Authorization/locks/write", - "Microsoft.Network/*/write", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/write", - "Microsoft.SecurityInsights/onboardingStates/write" - ] - not_actions = [] - } - } - } + default = {} } variable "custom_role_definitions_bicep" { @@ -707,11 +621,8 @@ variable "custom_role_definitions_bicep" { - `actions` (list(string)) - Allowed Azure actions - `not_actions` (list(string)) - Denied Azure actions - Default includes 4 predefined roles: - - `alz_management_group_contributor` - Manage management group hierarchy and governance - - `alz_management_group_reader` - Run Bicep What-If validations (requires --validation-level providerNoRbac flag) - - `alz_subscription_owner` - Full access to platform subscriptions - - `alz_subscription_reader` - Run Bicep What-If for subscription deployments + Default includes 1 predefined roles: + - `alz_reader` - Run Bicep What-If validations (requires --validation-level providerNoRbac flag)s See default value for complete role action definitions. EOT @@ -724,25 +635,7 @@ variable "custom_role_definitions_bicep" { }) })) default = { - alz_management_group_contributor = { - name = "Azure Landing Zones Management Group Contributor ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for creating and managing the Management Group hierarchy and its associated governance resources such as policy, RBAC etc..." - permissions = { - actions = [ - "*/read", - "Microsoft.Management/*", - "Microsoft.Authorization/*", - "Microsoft.Resources/*", - "Microsoft.Support/*", - "Microsoft.Insights/diagnosticSettings/*" - ] - not_actions = [ - "Microsoft.Resources/subscriptions/resourceGroups/write", - "Microsoft.Resources/subscriptions/resourceGroups/delete" - ] - } - } - alz_management_group_reader = { + alz_reader = { name = "Azure Landing Zones Management Group What If ({{service_name}}-{{environment_name}})" description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." permissions = { @@ -756,30 +649,6 @@ variable "custom_role_definitions_bicep" { not_actions = [] } } - alz_subscription_owner = { - name = "Azure Landing Zones Subscription Owner ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing in platform subscriptions." - permissions = { - actions = [ - "*" - ] - not_actions = [] - } - } - alz_subscription_reader = { - name = "Azure Landing Zones Subscription What If ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." - permissions = { - actions = [ - "*/read", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/validate/action", - "Microsoft.Resources/subscriptions/operationResults/read", - "Microsoft.Management/operationResults/*/read" - ] - not_actions = [] - } - } } } @@ -909,50 +778,83 @@ variable "role_assignments_terraform" { Map of role assignment configurations where: - **Key**: Assignment identifier (e.g., 'plan_management_group') - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') - `custom_role_definition_key` (string) - Key from custom_role_definitions_terraform - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') - `scope` (string) - Assignment scope ('management_group' or 'subscription') - Default includes 4 assignments: - - Plan and apply access for management group operations - - Plan and apply access for subscription operations + Default includes 2 assignments: + - Plan and apply access + EOT type = map(object({ - custom_role_definition_key = string + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) user_assigned_managed_identity_key = string scope = string })) default = { - plan_management_group = { - custom_role_definition_key = "alz_management_group_reader" + plan = { + built_in_role_definition_name = "Reader" user_assigned_managed_identity_key = "plan" scope = "management_group" } - apply_management_group = { - custom_role_definition_key = "alz_management_group_contributor" + apply = { + built_in_role_definition_name = "Owner" user_assigned_managed_identity_key = "apply" scope = "management_group" } - plan_subscription = { - custom_role_definition_key = "alz_subscription_reader" + } +} + +variable "role_assignments_bicep" { + description = <<-EOT + **(Optional)** RBAC role assignments for Bicep-based deployments. + + Map of role assignment configurations where: + - **Key**: Assignment identifier (e.g., 'plan_management_group') + - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') + - `custom_role_definition_key` (string) - Key from custom_role_definitions_bicep + - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') + - `scope` (string) - Assignment scope ('management_group' or 'subscription') + + Default includes 3 assignments: + - Plan and apply access operations + EOT + type = map(object({ + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) + user_assigned_managed_identity_key = string + scope = string + })) + default = { + plan = { + custom_role_definition_key = "Reader" user_assigned_managed_identity_key = "plan" - scope = "subscription" + scope = "management_group" } - apply_subscription = { - custom_role_definition_key = "alz_subscription_owner" + plan_custom = { + custom_role_definition_key = "alz_reader" + user_assigned_managed_identity_key = "plan" + scope = "management_group" + } + apply_management_group = { + custom_role_definition_key = "Owner" user_assigned_managed_identity_key = "apply" - scope = "subscription" + scope = "management_group" } } } -variable "role_assignments_bicep" { +variable "role_assignments_bicep_classic" { description = <<-EOT - **(Optional)** RBAC role assignments for Bicep-based deployments. + **(Optional)** RBAC role assignments for Bicep Classic based deployments. Map of role assignment configurations where: - **Key**: Assignment identifier (e.g., 'plan_management_group') - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') - `custom_role_definition_key` (string) - Key from custom_role_definitions_bicep - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') - `scope` (string) - Assignment scope ('management_group' or 'subscription') @@ -962,7 +864,8 @@ variable "role_assignments_bicep" { - Plan and apply access for subscription operations EOT type = map(object({ - custom_role_definition_key = string + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) user_assigned_managed_identity_key = string scope = string })) @@ -1078,3 +981,14 @@ variable "bicep_tenant_role_assignment_role_definition_name" { type = string default = "Landing Zone Management Owner" } + +variable "terraform_architecture_file_path" { + description = <<-EOT + **(Required)** Relative path to the Terraform architecture definition JSON file within the module folder. + + This file defines the structure and components of the Terraform deployment architecture. + Used for dynamic file manipulation based on architecture specifics. + EOT + type = string + default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" +} diff --git a/alz/github/main.tf b/alz/github/main.tf index 478b6f41..dc8ec6cf 100644 --- a/alz/github/main.tf +++ b/alz/github/main.tf @@ -60,7 +60,7 @@ module "azure" { container_registry_dockerfile_name = var.runner_container_image_dockerfile container_registry_dockerfile_repository_folder_url = local.runner_container_instance_dockerfile_url custom_role_definitions = var.iac_type == "terraform" ? local.custom_role_definitions_terraform : (var.iac_type == "bicep" ? local.custom_role_definitions_bicep : local.custom_role_definitions_bicep_classic) - role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : var.role_assignments_bicep + role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : (var.iac_type == "bicep" ? var.role_assignments_bicep : var.role_assignments_bicep_classic) storage_account_blob_soft_delete_enabled = var.storage_account_blob_soft_delete_enabled storage_account_blob_soft_delete_retention_days = var.storage_account_blob_soft_delete_retention_days storage_account_blob_versioning_enabled = var.storage_account_blob_versioning_enabled @@ -68,6 +68,9 @@ module "azure" { storage_account_container_soft_delete_retention_days = var.storage_account_container_soft_delete_retention_days tenant_role_assignment_enabled = var.iac_type == "bicep" && var.bicep_tenant_role_assignment_enabled tenant_role_assignment_role_definition_name = var.bicep_tenant_role_assignment_role_definition_name + intermediate_root_management_group_creation_enabled = var.iac_type != "bicep-classic" + intermediate_root_management_group_id = module.file_manipulation.intermediate_root_management_group_id + intermediate_root_management_group_display_name = module.file_manipulation.intermediate_root_management_group_display_name } module "github" { @@ -122,4 +125,5 @@ module "file_manipulation" { pipeline_files_directory_path = local.pipeline_files_directory_path pipeline_template_files_directory_path = local.pipeline_template_files_directory_path concurrency_value = local.resource_names.storage_container + terraform_architecture_file_path = var.terraform_architecture_file_path } diff --git a/alz/github/variables.tf b/alz/github/variables.tf index 8567c8ca..28d7a935 100644 --- a/alz/github/variables.tf +++ b/alz/github/variables.tf @@ -645,11 +645,7 @@ variable "custom_role_definitions_terraform" { - `actions` (list(string)) - Allowed Azure actions - `not_actions` (list(string)) - Denied Azure actions - Default includes 4 predefined roles: - - `alz_management_group_contributor` - Manage management group hierarchy and governance - - `alz_management_group_reader` - Read management group structure and validate deployments - - `alz_subscription_owner` - Full access to platform subscriptions - - `alz_subscription_reader` - Read/write access for platform subscription resources + Default is empty, meaning no custom roles are created. See default value for complete role action definitions. EOT @@ -661,89 +657,7 @@ variable "custom_role_definitions_terraform" { not_actions = list(string) }) })) - default = { - alz_management_group_contributor = { - name = "Azure Landing Zones Management Group Contributor ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing the Management Group Structure." - permissions = { - actions = [ - "Microsoft.Management/managementGroups/delete", - "Microsoft.Management/managementGroups/read", - "Microsoft.Management/managementGroups/subscriptions/delete", - "Microsoft.Management/managementGroups/subscriptions/write", - "Microsoft.Management/managementGroups/settings/read", - "Microsoft.Management/managementGroups/settings/write", - "Microsoft.Management/managementGroups/settings/delete", - "Microsoft.Management/managementGroups/write", - "Microsoft.Management/managementGroups/subscriptions/read", - "Microsoft.Authorization/policyDefinitions/write", - "Microsoft.Authorization/policySetDefinitions/write", - "Microsoft.Authorization/policyAssignments/write", - "Microsoft.Authorization/roleDefinitions/write", - "Microsoft.Authorization/*/read", - "Microsoft.Authorization/roleAssignments/write", - "Microsoft.Authorization/roleAssignments/delete", - "Microsoft.Insights/diagnosticSettings/write" - ] - not_actions = [] - } - } - alz_management_group_reader = { - name = "Azure Landing Zones Management Group Reader ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Reading the Management Group Structure." - permissions = { - actions = [ - "Microsoft.Management/managementGroups/read", - "Microsoft.Management/managementGroups/subscriptions/read", - "Microsoft.Management/managementGroups/settings/read", - "Microsoft.Authorization/*/read", - "Microsoft.Authorization/policyDefinitions/write", - "Microsoft.Authorization/policySetDefinitions/write", - "Microsoft.Authorization/roleDefinitions/write", - "Microsoft.Authorization/policyAssignments/write", - "Microsoft.Insights/diagnosticSettings/write", - "Microsoft.Insights/diagnosticSettings/read", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/write", - "Microsoft.Resources/deploymentStacks/read", - "Microsoft.Resources/deploymentStacks/validate/action" - ] - not_actions = [] - } - } - alz_subscription_owner = { - name = "Azure Landing Zones Subscription Owner ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing in platform subscriptions." - permissions = { - actions = [ - "*" - ] - not_actions = [] - } - } - alz_subscription_reader = { - name = "Azure Landing Zones Subscription Reader ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Reading the platform subscriptions." - permissions = { - actions = [ - "*/read", - "Microsoft.Resources/subscriptions/resourceGroups/write", - "Microsoft.ManagedIdentity/userAssignedIdentities/write", - "Microsoft.Automation/automationAccounts/write", - "Microsoft.OperationalInsights/workspaces/write", - "Microsoft.OperationalInsights/workspaces/linkedServices/write", - "Microsoft.OperationsManagement/solutions/write", - "Microsoft.Insights/dataCollectionRules/write", - "Microsoft.Authorization/locks/write", - "Microsoft.Network/*/write", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/write", - "Microsoft.SecurityInsights/onboardingStates/write" - ] - not_actions = [] - } - } - } + default = {} } variable "custom_role_definitions_bicep" { @@ -759,11 +673,8 @@ variable "custom_role_definitions_bicep" { - `actions` (list(string)) - Allowed Azure actions - `not_actions` (list(string)) - Denied Azure actions - Default includes 4 predefined roles: - - `alz_management_group_contributor` - Manage management group hierarchy and governance - - `alz_management_group_reader` - Run Bicep What-If validations (requires --validation-level providerNoRbac flag) - - `alz_subscription_owner` - Full access to platform subscriptions - - `alz_subscription_reader` - Run Bicep What-If for subscription deployments + Default includes 1 predefined roles: + - `alz_reader` - Run Bicep What-If validations (requires --validation-level providerNoRbac flag)s See default value for complete role action definitions. EOT @@ -776,25 +687,7 @@ variable "custom_role_definitions_bicep" { }) })) default = { - alz_management_group_contributor = { - name = "Azure Landing Zones Management Group Contributor ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for creating and managing the Management Group hierarchy and its associated governance resources such as policy, RBAC etc..." - permissions = { - actions = [ - "*/read", - "Microsoft.Management/*", - "Microsoft.Authorization/*", - "Microsoft.Resources/*", - "Microsoft.Support/*", - "Microsoft.Insights/diagnosticSettings/*" - ] - not_actions = [ - "Microsoft.Resources/subscriptions/resourceGroups/write", - "Microsoft.Resources/subscriptions/resourceGroups/delete" - ] - } - } - alz_management_group_reader = { + alz_reader = { name = "Azure Landing Zones Management Group What If ({{service_name}}-{{environment_name}})" description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." permissions = { @@ -808,30 +701,6 @@ variable "custom_role_definitions_bicep" { not_actions = [] } } - alz_subscription_owner = { - name = "Azure Landing Zones Subscription Owner ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing in platform subscriptions." - permissions = { - actions = [ - "*" - ] - not_actions = [] - } - } - alz_subscription_reader = { - name = "Azure Landing Zones Subscription What If ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." - permissions = { - actions = [ - "*/read", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/validate/action", - "Microsoft.Resources/subscriptions/operationResults/read", - "Microsoft.Management/operationResults/*/read" - ] - not_actions = [] - } - } } } @@ -961,50 +830,83 @@ variable "role_assignments_terraform" { Map of role assignment configurations where: - **Key**: Assignment identifier (e.g., 'plan_management_group') - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') - `custom_role_definition_key` (string) - Key from custom_role_definitions_terraform - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') - `scope` (string) - Assignment scope ('management_group' or 'subscription') - Default includes 4 assignments: - - Plan and apply access for management group operations - - Plan and apply access for subscription operations + Default includes 2 assignments: + - Plan and apply access + EOT type = map(object({ - custom_role_definition_key = string + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) user_assigned_managed_identity_key = string scope = string })) default = { - plan_management_group = { - custom_role_definition_key = "alz_management_group_reader" + plan = { + built_in_role_definition_name = "Reader" user_assigned_managed_identity_key = "plan" scope = "management_group" } - apply_management_group = { - custom_role_definition_key = "alz_management_group_contributor" + apply = { + built_in_role_definition_name = "Owner" user_assigned_managed_identity_key = "apply" scope = "management_group" } - plan_subscription = { - custom_role_definition_key = "alz_subscription_reader" + } +} + +variable "role_assignments_bicep" { + description = <<-EOT + **(Optional)** RBAC role assignments for Bicep-based deployments. + + Map of role assignment configurations where: + - **Key**: Assignment identifier (e.g., 'plan_management_group') + - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') + - `custom_role_definition_key` (string) - Key from custom_role_definitions_bicep + - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') + - `scope` (string) - Assignment scope ('management_group' or 'subscription') + + Default includes 3 assignments: + - Plan and apply access operations + EOT + type = map(object({ + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) + user_assigned_managed_identity_key = string + scope = string + })) + default = { + plan = { + custom_role_definition_key = "Reader" user_assigned_managed_identity_key = "plan" - scope = "subscription" + scope = "management_group" } - apply_subscription = { - custom_role_definition_key = "alz_subscription_owner" + plan_custom = { + custom_role_definition_key = "alz_reader" + user_assigned_managed_identity_key = "plan" + scope = "management_group" + } + apply_management_group = { + custom_role_definition_key = "Owner" user_assigned_managed_identity_key = "apply" - scope = "subscription" + scope = "management_group" } } } -variable "role_assignments_bicep" { +variable "role_assignments_bicep_classic" { description = <<-EOT - **(Optional)** RBAC role assignments for Bicep-based deployments. + **(Optional)** RBAC role assignments for Bicep Classic based deployments. Map of role assignment configurations where: - **Key**: Assignment identifier (e.g., 'plan_management_group') - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') - `custom_role_definition_key` (string) - Key from custom_role_definitions_bicep - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') - `scope` (string) - Assignment scope ('management_group' or 'subscription') @@ -1014,7 +916,8 @@ variable "role_assignments_bicep" { - Plan and apply access for subscription operations EOT type = map(object({ - custom_role_definition_key = string + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) user_assigned_managed_identity_key = string scope = string })) @@ -1130,3 +1033,14 @@ variable "bicep_tenant_role_assignment_role_definition_name" { type = string default = "Landing Zone Management Owner" } + +variable "terraform_architecture_file_path" { + description = <<-EOT + **(Required)** Relative path to the Terraform architecture definition JSON file within the module folder. + + This file defines the structure and components of the Terraform deployment architecture. + Used for dynamic file manipulation based on architecture specifics. + EOT + type = string + default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" +} diff --git a/alz/local/main.tf b/alz/local/main.tf index 27e91049..0e38dc6f 100644 --- a/alz/local/main.tf +++ b/alz/local/main.tf @@ -33,7 +33,7 @@ module "azure" { use_self_hosted_agents = false use_private_networking = false custom_role_definitions = var.iac_type == "terraform" ? local.custom_role_definitions_terraform : (var.iac_type == "bicep" ? local.custom_role_definitions_bicep : local.custom_role_definitions_bicep_classic) - role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : var.role_assignments_bicep + role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : (var.iac_type == "bicep" ? var.role_assignments_bicep : var.role_assignments_bicep_classic) additional_role_assignment_principal_ids = var.grant_permissions_to_current_user ? { current_user = data.azurerm_client_config.current.object_id } : {} storage_account_blob_soft_delete_enabled = var.storage_account_blob_soft_delete_enabled storage_account_blob_soft_delete_retention_days = var.storage_account_blob_soft_delete_retention_days @@ -42,6 +42,9 @@ module "azure" { storage_account_container_soft_delete_retention_days = var.storage_account_container_soft_delete_retention_days tenant_role_assignment_enabled = var.iac_type == "bicep" && var.bicep_tenant_role_assignment_enabled tenant_role_assignment_role_definition_name = var.bicep_tenant_role_assignment_role_definition_name + intermediate_root_management_group_creation_enabled = var.iac_type != "bicep-classic" + intermediate_root_management_group_id = module.file_manipulation.intermediate_root_management_group_id + intermediate_root_management_group_display_name = module.file_manipulation.intermediate_root_management_group_display_name } module "file_manipulation" { @@ -59,6 +62,7 @@ module "file_manipulation" { pipeline_target_folder_name = local.script_target_folder_name bicep_parameters_file_path = var.bicep_parameters_file_path pipeline_files_directory_path = local.script_source_folder_path + terraform_architecture_file_path = var.terraform_architecture_file_path } resource "local_file" "alz" { diff --git a/alz/local/variables.tf b/alz/local/variables.tf index e714b93c..d1a4968a 100644 --- a/alz/local/variables.tf +++ b/alz/local/variables.tf @@ -358,11 +358,7 @@ variable "custom_role_definitions_terraform" { - `actions` (list(string)) - Allowed Azure actions - `not_actions` (list(string)) - Denied Azure actions - Default includes 4 predefined roles: - - `alz_management_group_contributor` - Manage management group hierarchy and governance - - `alz_management_group_reader` - Read management group structure and validate deployments - - `alz_subscription_owner` - Full access to platform subscriptions - - `alz_subscription_reader` - Read/write access for platform subscription resources + Default is empty, meaning no custom roles are created. See default value for complete role action definitions. EOT @@ -374,89 +370,7 @@ variable "custom_role_definitions_terraform" { not_actions = list(string) }) })) - default = { - alz_management_group_contributor = { - name = "Azure Landing Zones Management Group Contributor ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing the Management Group Structure." - permissions = { - actions = [ - "Microsoft.Management/managementGroups/delete", - "Microsoft.Management/managementGroups/read", - "Microsoft.Management/managementGroups/subscriptions/delete", - "Microsoft.Management/managementGroups/subscriptions/write", - "Microsoft.Management/managementGroups/settings/read", - "Microsoft.Management/managementGroups/settings/write", - "Microsoft.Management/managementGroups/settings/delete", - "Microsoft.Management/managementGroups/write", - "Microsoft.Management/managementGroups/subscriptions/read", - "Microsoft.Authorization/policyDefinitions/write", - "Microsoft.Authorization/policySetDefinitions/write", - "Microsoft.Authorization/policyAssignments/write", - "Microsoft.Authorization/roleDefinitions/write", - "Microsoft.Authorization/*/read", - "Microsoft.Authorization/roleAssignments/write", - "Microsoft.Authorization/roleAssignments/delete", - "Microsoft.Insights/diagnosticSettings/write" - ] - not_actions = [] - } - } - alz_management_group_reader = { - name = "Azure Landing Zones Management Group Reader ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Reading the Management Group Structure." - permissions = { - actions = [ - "Microsoft.Management/managementGroups/read", - "Microsoft.Management/managementGroups/subscriptions/read", - "Microsoft.Management/managementGroups/settings/read", - "Microsoft.Authorization/*/read", - "Microsoft.Authorization/policyDefinitions/write", - "Microsoft.Authorization/policySetDefinitions/write", - "Microsoft.Authorization/roleDefinitions/write", - "Microsoft.Authorization/policyAssignments/write", - "Microsoft.Insights/diagnosticSettings/write", - "Microsoft.Insights/diagnosticSettings/read", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/write", - "Microsoft.Resources/deploymentStacks/read", - "Microsoft.Resources/deploymentStacks/validate/action" - ] - not_actions = [] - } - } - alz_subscription_owner = { - name = "Azure Landing Zones Subscription Owner ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing in platform subscriptions." - permissions = { - actions = [ - "*" - ] - not_actions = [] - } - } - alz_subscription_reader = { - name = "Azure Landing Zones Subscription Reader ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Reading the platform subscriptions." - permissions = { - actions = [ - "*/read", - "Microsoft.Resources/subscriptions/resourceGroups/write", - "Microsoft.ManagedIdentity/userAssignedIdentities/write", - "Microsoft.Automation/automationAccounts/write", - "Microsoft.OperationalInsights/workspaces/write", - "Microsoft.OperationalInsights/workspaces/linkedServices/write", - "Microsoft.OperationsManagement/solutions/write", - "Microsoft.Insights/dataCollectionRules/write", - "Microsoft.Authorization/locks/write", - "Microsoft.Network/*/write", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/write", - "Microsoft.SecurityInsights/onboardingStates/write" - ] - not_actions = [] - } - } - } + default = {} } variable "custom_role_definitions_bicep" { @@ -472,11 +386,8 @@ variable "custom_role_definitions_bicep" { - `actions` (list(string)) - Allowed Azure actions - `not_actions` (list(string)) - Denied Azure actions - Default includes 4 predefined roles: - - `alz_management_group_contributor` - Manage management group hierarchy and governance - - `alz_management_group_reader` - Run Bicep What-If validations (requires --validation-level providerNoRbac flag) - - `alz_subscription_owner` - Full access to platform subscriptions - - `alz_subscription_reader` - Run Bicep What-If for subscription deployments + Default includes 1 predefined roles: + - `alz_reader` - Run Bicep What-If validations (requires --validation-level providerNoRbac flag)s See default value for complete role action definitions. EOT @@ -489,25 +400,7 @@ variable "custom_role_definitions_bicep" { }) })) default = { - alz_management_group_contributor = { - name = "Azure Landing Zones Management Group Contributor ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for creating and managing the Management Group hierarchy and its associated governance resources such as policy, RBAC etc..." - permissions = { - actions = [ - "*/read", - "Microsoft.Management/*", - "Microsoft.Authorization/*", - "Microsoft.Resources/*", - "Microsoft.Support/*", - "Microsoft.Insights/diagnosticSettings/*" - ] - not_actions = [ - "Microsoft.Resources/subscriptions/resourceGroups/write", - "Microsoft.Resources/subscriptions/resourceGroups/delete" - ] - } - } - alz_management_group_reader = { + alz_reader = { name = "Azure Landing Zones Management Group What If ({{service_name}}-{{environment_name}})" description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." permissions = { @@ -521,30 +414,6 @@ variable "custom_role_definitions_bicep" { not_actions = [] } } - alz_subscription_owner = { - name = "Azure Landing Zones Subscription Owner ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for Writing in platform subscriptions." - permissions = { - actions = [ - "*" - ] - not_actions = [] - } - } - alz_subscription_reader = { - name = "Azure Landing Zones Subscription What If ({{service_name}}-{{environment_name}})" - description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." - permissions = { - actions = [ - "*/read", - "Microsoft.Resources/deployments/whatIf/action", - "Microsoft.Resources/deployments/validate/action", - "Microsoft.Resources/subscriptions/operationResults/read", - "Microsoft.Management/operationResults/*/read" - ] - not_actions = [] - } - } } } @@ -674,50 +543,83 @@ variable "role_assignments_terraform" { Map of role assignment configurations where: - **Key**: Assignment identifier (e.g., 'plan_management_group') - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') - `custom_role_definition_key` (string) - Key from custom_role_definitions_terraform - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') - `scope` (string) - Assignment scope ('management_group' or 'subscription') - Default includes 4 assignments: - - Plan and apply access for management group operations - - Plan and apply access for subscription operations + Default includes 2 assignments: + - Plan and apply access + EOT type = map(object({ - custom_role_definition_key = string + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) user_assigned_managed_identity_key = string scope = string })) default = { - plan_management_group = { - custom_role_definition_key = "alz_management_group_reader" + plan = { + built_in_role_definition_name = "Reader" user_assigned_managed_identity_key = "plan" scope = "management_group" } - apply_management_group = { - custom_role_definition_key = "alz_management_group_contributor" + apply = { + built_in_role_definition_name = "Owner" user_assigned_managed_identity_key = "apply" scope = "management_group" } - plan_subscription = { - custom_role_definition_key = "alz_subscription_reader" + } +} + +variable "role_assignments_bicep" { + description = <<-EOT + **(Optional)** RBAC role assignments for Bicep-based deployments. + + Map of role assignment configurations where: + - **Key**: Assignment identifier (e.g., 'plan_management_group') + - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') + - `custom_role_definition_key` (string) - Key from custom_role_definitions_bicep + - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') + - `scope` (string) - Assignment scope ('management_group' or 'subscription') + + Default includes 3 assignments: + - Plan and apply access operations + EOT + type = map(object({ + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) + user_assigned_managed_identity_key = string + scope = string + })) + default = { + plan = { + custom_role_definition_key = "Reader" user_assigned_managed_identity_key = "plan" - scope = "subscription" + scope = "management_group" } - apply_subscription = { - custom_role_definition_key = "alz_subscription_owner" + plan_custom = { + custom_role_definition_key = "alz_reader" + user_assigned_managed_identity_key = "plan" + scope = "management_group" + } + apply_management_group = { + custom_role_definition_key = "Owner" user_assigned_managed_identity_key = "apply" - scope = "subscription" + scope = "management_group" } } } -variable "role_assignments_bicep" { +variable "role_assignments_bicep_classic" { description = <<-EOT - **(Optional)** RBAC role assignments for Bicep-based deployments. + **(Optional)** RBAC role assignments for Bicep Classic based deployments. Map of role assignment configurations where: - **Key**: Assignment identifier (e.g., 'plan_management_group') - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (e.g., 'Owner', 'Contributor') - `custom_role_definition_key` (string) - Key from custom_role_definitions_bicep - `user_assigned_managed_identity_key` (string) - Managed identity key ('plan' or 'apply') - `scope` (string) - Assignment scope ('management_group' or 'subscription') @@ -727,7 +629,8 @@ variable "role_assignments_bicep" { - Plan and apply access for subscription operations EOT type = map(object({ - custom_role_definition_key = string + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) user_assigned_managed_identity_key = string scope = string })) @@ -829,3 +732,14 @@ variable "bicep_tenant_role_assignment_role_definition_name" { description = "The name of the Azure role definition to assign at the tenant level for Bicep deployments. This role grants the managed identity permissions to manage Azure Landing Zones resources across the tenant. Common values: 'Landing Zone Management Owner', 'Owner', or a custom role name." default = "Landing Zone Management Owner" } + +variable "terraform_architecture_file_path" { + description = <<-EOT + **(Required)** Relative path to the Terraform architecture definition JSON file within the module folder. + + This file defines the structure and components of the Terraform deployment architecture. + Used for dynamic file manipulation based on architecture specifics. + EOT + type = string + default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" +} diff --git a/modules/azure/management_group.tf b/modules/azure/management_group.tf new file mode 100644 index 00000000..df6c175a --- /dev/null +++ b/modules/azure/management_group.tf @@ -0,0 +1,34 @@ +resource "azapi_resource" "intermediate_root_management_group" { + count = var.intermediate_root_management_group_creation_enabled ? 1 : 0 + name = var.intermediate_root_management_group_id + parent_id = "/" + type = "Microsoft.Management/managementGroups@2023-04-01" + body = { + properties = { + details = { + parent = { + id = "/providers/Microsoft.Management/managementGroups/${var.root_parent_management_group_id}" + } + } + displayName = var.intermediate_root_management_group_display_name + } + } + + replace_triggers_external_values = [ + var.root_parent_management_group_id, + ] + response_export_values = [] + retry = { + error_message_regex = [ + "AuthorizationFailed", # Avoids a eventual consistency issue where a recently created management group is not yet available for a GET operation. + "Permission to Microsoft.Management/managementGroups on resources of type 'Write' is required on the management group or its ancestors." + ] + } + + timeouts { + create = "60m" + delete = "5m" + read = "60m" + update = "5m" + } +} diff --git a/modules/azure/role_assignments.tf b/modules/azure/role_assignments.tf index 3f1c3e02..8f623c58 100644 --- a/modules/azure/role_assignments.tf +++ b/modules/azure/role_assignments.tf @@ -1,6 +1,7 @@ locals { role_assignments = { for key, value in var.role_assignments : key => { user_assigned_managed_identity_key = value.user_assigned_managed_identity_key + built_in_role_definition_name = value.built_in_role_definition_name custom_role_definition_key = value.custom_role_definition_key scope = value.scope principal_id = azurerm_user_assigned_identity.alz[value.user_assigned_managed_identity_key].principal_id @@ -11,12 +12,14 @@ locals { for princial_key, principal_value in var.additional_role_assignment_principal_ids : { composite_key = "${value.scope}-${value.custom_role_definition_key}-${princial_key}" user_assigned_managed_identity_key = "${value.scope}-${value.custom_role_definition_key}-${princial_key}" + built_in_role_definition_name = value.built_in_role_definition_name custom_role_definition_key = value.custom_role_definition_key scope = value.scope principal_id = principal_value } ]]) : assignment.composite_key => { user_assigned_managed_identity_key = assignment.user_assigned_managed_identity_key + built_in_role_definition_name = assignment.built_in_role_definition_name custom_role_definition_key = assignment.custom_role_definition_key scope = assignment.scope principal_id = assignment.principal_id @@ -27,10 +30,11 @@ locals { subscription_role_assignments = { for assignment in flatten([ for key, value in local.combined_role_assignments : [ for subscription_id, subscription in data.azurerm_subscription.alz : { - key = "${value.user_assigned_managed_identity_key}-${value.custom_role_definition_key}-${subscription_id}" - scope = subscription.id - role_definition_id = "${subscription.id}${azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id}" - principal_id = value.principal_id + key = "${value.user_assigned_managed_identity_key}-${value.custom_role_definition_key}-${subscription_id}" + scope = subscription.id + role_definition_id = value.built_in_role_definition_name ? "${subscription.id}${azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id}" : null + role_definition_name = value.built_in_role_definition_name + principal_id = value.principal_id } ] if value.scope == "subscription" ]) : assignment.key => { @@ -41,19 +45,21 @@ locals { management_group_role_assignments = { for key, value in local.combined_role_assignments : key => { - scope = data.azurerm_management_group.alz.id - role_definition_id = azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id - principal_id = value.principal_id + scope = var.intermediate_root_management_group_creation_enabled ? azapi_resource.intermediate_root_management_group.id : data.azurerm_management_group.alz.id + role_definition_id = value.built_in_role_definition_name == null ? azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id : null + role_definition_name = value.built_in_role_definition_name + principal_id = value.principal_id } if value.scope == "management_group" } final_role_assignments = merge(local.subscription_role_assignments, local.management_group_role_assignments) } resource "azurerm_role_assignment" "alz" { - for_each = local.final_role_assignments - scope = each.value.scope - role_definition_id = each.value.role_definition_id - principal_id = each.value.principal_id + for_each = local.final_role_assignments + scope = each.value.scope + role_definition_id = each.value.role_definition_id + role_definition_name = each.value.role_definition_name + principal_id = each.value.principal_id } # Bicep needs some permissions at tenant level to deploy management groups and policy in the same deployment diff --git a/modules/azure/role_definitions.tf b/modules/azure/role_definitions.tf index cc01271c..c19c1114 100644 --- a/modules/azure/role_definitions.tf +++ b/modules/azure/role_definitions.tf @@ -1,7 +1,7 @@ resource "azurerm_role_definition" "alz" { for_each = var.custom_role_definitions name = each.value.name - scope = data.azurerm_management_group.alz.id + scope = var.intermediate_root_management_group_creation_enabled ? azapi_resource.intermediate_root_management_group.id : data.azurerm_management_group.alz.id description = each.value.description permissions { diff --git a/modules/azure/variables.tf b/modules/azure/variables.tf index 80d70875..513686c3 100644 --- a/modules/azure/variables.tf +++ b/modules/azure/variables.tf @@ -271,6 +271,36 @@ variable "root_parent_management_group_id" { type = string } +variable "intermediate_root_management_group_creation_enabled" { + description = <<-EOT + **(Optional, default: `true`)** Controls whether to create an intermediate root management group under the root parent. + + When enabled, creates a dedicated management group to serve as the root for all Azure Landing Zones management groups and subscriptions. + Helps isolate landing zone resources from other management groups in the tenant. + EOT + type = bool + default = true +} + +variable "intermediate_root_management_group_id" { + description = <<-EOT + **(Required)** The ID of the intermediate root management group to create under the root parent. + + This management group serves as the root for all Azure Landing Zones management groups and subscriptions. + Must be unique within the tenant. + EOT + type = string +} + +variable "intermediate_root_management_group_display_name" { + description = <<-EOT + **(Required)** The display name for the intermediate root management group. + + This is a human-readable name shown in the Azure portal for the management group. + EOT + type = string +} + variable "resource_providers" { description = <<-EOT **(Optional, default: comprehensive list)** The resource providers to register in the Azure subscription. @@ -556,12 +586,14 @@ variable "role_assignments" { Map structure: - **Key**: Assignment identifier (e.g., 'plan_management_group') - **Value**: Object containing: + - `built_in_role_definition_name` (string) - Name of built-in role (optional) - `custom_role_definition_key` (string) - Key from custom_role_definitions - `user_assigned_managed_identity_key` (string) - Key from user_assigned_managed_identities - `scope` (string) - Assignment scope ('management_group' or 'subscription') EOT type = map(object({ - custom_role_definition_key = string + built_in_role_definition_name = optional(string) + custom_role_definition_key = optional(string) user_assigned_managed_identity_key = string scope = string })) diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf new file mode 100644 index 00000000..ab6e121b --- /dev/null +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -0,0 +1,27 @@ +locals { + is_terraform_iac_type = var.iac_type == "terraform" + terraform_architecture = local.is_terraform_iac_type ? endswith(var.terraform_architecture_file_path, ".yaml") || endswith(var.terraform_architecture_file_path, ".json") ? yamldecode(file("${var.module_folder_path}/${var.terraform_architecture_file_path}")) : jsondecode(file("${var.module_folder_path}/${var.terraform_architecture_file_path}")) : null + terraform_intermediate_root_management_group = local.is_terraform_iac_type ? ({ for management_group in local.terraform_architecture.management_groups : management_group.id => management_group if management_group.parent_id == null })[0] : null + intermediate_root_management_group = local.is_terraform_iac_type ? { + id = local.terraform_intermediate_root_management_group.id + display_name = local.terraform_intermediate_root_management_group.display_name + } : { + id = try("${local.bicep_parameters.management_group_id_prefix}${local.bicep_parameters.management_group_int_root_id}${local.bicep_parameters.management_group_id_postfix}", "") + display_name = try("${local.bicep_parameters.management_group_name_prefix}${local.bicep_parameters.management_group_int_root_name}${local.bicep_parameters.management_group_name_postfix}", "") + } +} + +locals { + import_block = < Date: Thu, 15 Jan 2026 13:01:20 +0000 Subject: [PATCH 02/42] fmt --- .../locals.intermediate_root_management_group.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf index ab6e121b..39933196 100644 --- a/modules/file_manipulation/locals.intermediate_root_management_group.tf +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -12,16 +12,16 @@ locals { } locals { - import_block = < Date: Thu, 15 Jan 2026 13:32:41 +0000 Subject: [PATCH 03/42] add lib to test --- .github/tests/scripts/generate-matrix.ps1 | 4 ++-- .github/workflows/end-to-end-test.yml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/tests/scripts/generate-matrix.ps1 b/.github/tests/scripts/generate-matrix.ps1 index ab8d2e79..bd67f437 100644 --- a/.github/tests/scripts/generate-matrix.ps1 +++ b/.github/tests/scripts/generate-matrix.ps1 @@ -48,7 +48,7 @@ $combinations = [ordered]@{ infrastructureAsCode = @("terraform") agentType = @("public", "private", "none") operatingSystem = @("ubuntu") - starterModule = @("test_nested") + starterModule = @("test") regions = @("multi") terraformVersion = @("latest") deployAzureResources = @("true") @@ -58,7 +58,7 @@ $combinations = [ordered]@{ infrastructureAsCode = @("terraform") agentType = @("public", "private", "none") operatingSystem = @("ubuntu") - starterModule = @("test_nested") + starterModule = @("test") regions = @("multi") terraformVersion = @("latest") deployAzureResources = @("true") diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 3b6ebad0..0d0eceae 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -297,6 +297,16 @@ jobs: $Inputs["child_management_group_display_name"] = "E2E Test" $Inputs["resource_group_location"] = $location + # Terraform + if($infrastructureAsCode -eq "terraform") { + $Inputs["resource_name_suffix"] = $uniqueId + $architectureFilePath = "${{ env.STARTER_MODULE_FOLDER }}/lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" + $architectureFile = Get-Content -Path $architectureFilePath -Raw + $architectureFile = $architectureFile.Replace("id: test", "id: test-$uniqueId") + $architectureFile = $architectureFile.Replace("display_name: Test", "display_name: Test $uniqueId") + $architectureFile | Out-File -FilePath $architectureFilePath -Encoding utf8 -Force + } + # Bicep Classic if($infrastructureAsCode -eq "bicep-classic") { $Inputs["Prefix"] = $uniqueId From 053046440bb77f6c494decc5f773ee881a3cc3c6 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Thu, 15 Jan 2026 13:33:09 +0000 Subject: [PATCH 04/42] fmt --- .../locals.intermediate_root_management_group.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf index 39933196..ab6e121b 100644 --- a/modules/file_manipulation/locals.intermediate_root_management_group.tf +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -12,16 +12,16 @@ locals { } locals { - import_block = < Date: Thu, 15 Jan 2026 13:39:19 +0000 Subject: [PATCH 05/42] typo --- modules/azure/role_assignments.tf | 2 +- modules/azure/role_definitions.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/azure/role_assignments.tf b/modules/azure/role_assignments.tf index 8f623c58..670ee8f8 100644 --- a/modules/azure/role_assignments.tf +++ b/modules/azure/role_assignments.tf @@ -45,7 +45,7 @@ locals { management_group_role_assignments = { for key, value in local.combined_role_assignments : key => { - scope = var.intermediate_root_management_group_creation_enabled ? azapi_resource.intermediate_root_management_group.id : data.azurerm_management_group.alz.id + scope = var.intermediate_root_management_group_creation_enabled ? azapi_resource.intermediate_root_management_group[0].id : data.azurerm_management_group.alz.id role_definition_id = value.built_in_role_definition_name == null ? azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id : null role_definition_name = value.built_in_role_definition_name principal_id = value.principal_id diff --git a/modules/azure/role_definitions.tf b/modules/azure/role_definitions.tf index c19c1114..953d7086 100644 --- a/modules/azure/role_definitions.tf +++ b/modules/azure/role_definitions.tf @@ -1,7 +1,7 @@ resource "azurerm_role_definition" "alz" { for_each = var.custom_role_definitions name = each.value.name - scope = var.intermediate_root_management_group_creation_enabled ? azapi_resource.intermediate_root_management_group.id : data.azurerm_management_group.alz.id + scope = var.intermediate_root_management_group_creation_enabled ? azapi_resource.intermediate_root_management_group[0].id : data.azurerm_management_group.alz.id description = each.value.description permissions { From 60cdac81433202e3fe8d17bf0d4b6a37880975c2 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Thu, 15 Jan 2026 13:43:53 +0000 Subject: [PATCH 06/42] fix path --- .github/workflows/end-to-end-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 0d0eceae..fa0763f5 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -300,7 +300,7 @@ jobs: # Terraform if($infrastructureAsCode -eq "terraform") { $Inputs["resource_name_suffix"] = $uniqueId - $architectureFilePath = "${{ env.STARTER_MODULE_FOLDER }}/lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" + $architectureFilePath = "${{ env.STARTER_MODULE_FOLDER }}/templates/$starterModule/lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" $architectureFile = Get-Content -Path $architectureFilePath -Raw $architectureFile = $architectureFile.Replace("id: test", "id: test-$uniqueId") $architectureFile = $architectureFile.Replace("display_name: Test", "display_name: Test $uniqueId") From e1b424f1d29df54dce365f059c171409d6241d32 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Thu, 15 Jan 2026 14:24:59 +0000 Subject: [PATCH 07/42] fix data type --- .../locals.intermediate_root_management_group.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf index ab6e121b..c576cd91 100644 --- a/modules/file_manipulation/locals.intermediate_root_management_group.tf +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -1,7 +1,7 @@ locals { is_terraform_iac_type = var.iac_type == "terraform" terraform_architecture = local.is_terraform_iac_type ? endswith(var.terraform_architecture_file_path, ".yaml") || endswith(var.terraform_architecture_file_path, ".json") ? yamldecode(file("${var.module_folder_path}/${var.terraform_architecture_file_path}")) : jsondecode(file("${var.module_folder_path}/${var.terraform_architecture_file_path}")) : null - terraform_intermediate_root_management_group = local.is_terraform_iac_type ? ({ for management_group in local.terraform_architecture.management_groups : management_group.id => management_group if management_group.parent_id == null })[0] : null + terraform_intermediate_root_management_group = local.is_terraform_iac_type ? ([ for management_group in local.terraform_architecture.management_groups : management_group if management_group.parent_id == null ])[0] : null intermediate_root_management_group = local.is_terraform_iac_type ? { id = local.terraform_intermediate_root_management_group.id display_name = local.terraform_intermediate_root_management_group.display_name From 9cc08195cafb46e721104ca1cc08f6ddfbd891a1 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Thu, 15 Jan 2026 14:29:00 +0000 Subject: [PATCH 08/42] fix roles --- alz/azuredevops/variables.tf | 7 ++----- alz/github/variables.tf | 7 ++----- alz/local/variables.tf | 7 ++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/alz/azuredevops/variables.tf b/alz/azuredevops/variables.tf index 394947c5..c7731217 100644 --- a/alz/azuredevops/variables.tf +++ b/alz/azuredevops/variables.tf @@ -640,11 +640,8 @@ variable "custom_role_definitions_bicep" { description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." permissions = { actions = [ - "*/read", "Microsoft.Resources/deployments/whatIf/action", "Microsoft.Resources/deployments/validate/action", - "Microsoft.Resources/subscriptions/operationResults/read", - "Microsoft.Management/operationResults/*/read" ] not_actions = [] } @@ -830,7 +827,7 @@ variable "role_assignments_bicep" { })) default = { plan = { - custom_role_definition_key = "Reader" + built_in_role_definition_name = "Reader" user_assigned_managed_identity_key = "plan" scope = "management_group" } @@ -840,7 +837,7 @@ variable "role_assignments_bicep" { scope = "management_group" } apply_management_group = { - custom_role_definition_key = "Owner" + built_in_role_definition_name = "Owner" user_assigned_managed_identity_key = "apply" scope = "management_group" } diff --git a/alz/github/variables.tf b/alz/github/variables.tf index 28d7a935..11dd9fca 100644 --- a/alz/github/variables.tf +++ b/alz/github/variables.tf @@ -692,11 +692,8 @@ variable "custom_role_definitions_bicep" { description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." permissions = { actions = [ - "*/read", "Microsoft.Resources/deployments/whatIf/action", "Microsoft.Resources/deployments/validate/action", - "Microsoft.Resources/subscriptions/operationResults/read", - "Microsoft.Management/operationResults/*/read" ] not_actions = [] } @@ -882,7 +879,7 @@ variable "role_assignments_bicep" { })) default = { plan = { - custom_role_definition_key = "Reader" + built_in_role_definition_name = "Reader" user_assigned_managed_identity_key = "plan" scope = "management_group" } @@ -892,7 +889,7 @@ variable "role_assignments_bicep" { scope = "management_group" } apply_management_group = { - custom_role_definition_key = "Owner" + built_in_role_definition_name = "Owner" user_assigned_managed_identity_key = "apply" scope = "management_group" } diff --git a/alz/local/variables.tf b/alz/local/variables.tf index d1a4968a..12eddf9c 100644 --- a/alz/local/variables.tf +++ b/alz/local/variables.tf @@ -405,11 +405,8 @@ variable "custom_role_definitions_bicep" { description = "This is a custom role created by the Azure Landing Zones Accelerator for running Bicep What If for the Management Group hierarchy and its associated governance resources such as policy, RBAC etc... You must use the `--validation-level providerNoRbac` (Az CLI 2.75.0 or later) or `-ValidationLevel providerNoRbac` (Az PowerShell 13.4.0 or later (Az.Resources 7.10.0 or later)) flag when running Bicep What If with this role." permissions = { actions = [ - "*/read", "Microsoft.Resources/deployments/whatIf/action", "Microsoft.Resources/deployments/validate/action", - "Microsoft.Resources/subscriptions/operationResults/read", - "Microsoft.Management/operationResults/*/read" ] not_actions = [] } @@ -595,7 +592,7 @@ variable "role_assignments_bicep" { })) default = { plan = { - custom_role_definition_key = "Reader" + built_in_role_definition_name = "Reader" user_assigned_managed_identity_key = "plan" scope = "management_group" } @@ -605,7 +602,7 @@ variable "role_assignments_bicep" { scope = "management_group" } apply_management_group = { - custom_role_definition_key = "Owner" + built_in_role_definition_name = "Owner" user_assigned_managed_identity_key = "apply" scope = "management_group" } From 98f9abd07eb4852040e886bd19b1217ca1d1e7fa Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Thu, 15 Jan 2026 14:55:26 +0000 Subject: [PATCH 09/42] fix formatting --- .../locals.intermediate_root_management_group.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf index c576cd91..c9d18b47 100644 --- a/modules/file_manipulation/locals.intermediate_root_management_group.tf +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -14,8 +14,8 @@ locals { locals { import_block = < Date: Thu, 15 Jan 2026 16:05:30 +0000 Subject: [PATCH 10/42] move subs --- alz/azuredevops/main.tf | 1 + alz/github/main.tf | 1 + alz/local/main.tf | 1 + modules/azure/subscription_placements.tf | 20 ++++++++++++++++++++ modules/azure/variables.tf | 11 +++++++++++ 5 files changed, 34 insertions(+) create mode 100644 modules/azure/subscription_placements.tf diff --git a/alz/azuredevops/main.tf b/alz/azuredevops/main.tf index 707212dc..48819723 100644 --- a/alz/azuredevops/main.tf +++ b/alz/azuredevops/main.tf @@ -70,6 +70,7 @@ module "azure" { intermediate_root_management_group_creation_enabled = var.iac_type != "bicep-classic" intermediate_root_management_group_id = module.file_manipulation.intermediate_root_management_group_id intermediate_root_management_group_display_name = module.file_manipulation.intermediate_root_management_group_display_name + move_subscriptions_to_target_management_group = var.iac_type != "bicep-classic" } module "azure_devops" { diff --git a/alz/github/main.tf b/alz/github/main.tf index dc8ec6cf..2249a74a 100644 --- a/alz/github/main.tf +++ b/alz/github/main.tf @@ -71,6 +71,7 @@ module "azure" { intermediate_root_management_group_creation_enabled = var.iac_type != "bicep-classic" intermediate_root_management_group_id = module.file_manipulation.intermediate_root_management_group_id intermediate_root_management_group_display_name = module.file_manipulation.intermediate_root_management_group_display_name + move_subscriptions_to_target_management_group = var.iac_type != "bicep-classic" } module "github" { diff --git a/alz/local/main.tf b/alz/local/main.tf index 0e38dc6f..9812d521 100644 --- a/alz/local/main.tf +++ b/alz/local/main.tf @@ -45,6 +45,7 @@ module "azure" { intermediate_root_management_group_creation_enabled = var.iac_type != "bicep-classic" intermediate_root_management_group_id = module.file_manipulation.intermediate_root_management_group_id intermediate_root_management_group_display_name = module.file_manipulation.intermediate_root_management_group_display_name + move_subscriptions_to_target_management_group = var.iac_type != "bicep-classic" } module "file_manipulation" { diff --git a/modules/azure/subscription_placements.tf b/modules/azure/subscription_placements.tf new file mode 100644 index 00000000..a1ec2a00 --- /dev/null +++ b/modules/azure/subscription_placements.tf @@ -0,0 +1,20 @@ +resource "azapi_resource" "subscription_placement" { + for_each = var.move_subscriptions_to_target_management_group ? { for subscription_id in var.target_subscriptions : subscription_id => subscription_id } : {} + + name = each.value + parent_id = var.intermediate_root_management_group_creation_enabled ? azapi_resource.intermediate_root_management_group[0].id : data.azurerm_management_group.alz.id + type = "Microsoft.Management/managementGroups/subscriptions@2023-04-01" + response_export_values = [] + retry = { + error_message_regex = [ + "AuthorizationFailed", # Avoids a eventual consistency issue where a recently created management group is not yet available for a GET operation. + ] + } + + timeouts { + create = "60m" + delete = "5m" + read = "60m" + update = "5m" + } +} diff --git a/modules/azure/variables.tf b/modules/azure/variables.tf index 513686c3..c44c84da 100644 --- a/modules/azure/variables.tf +++ b/modules/azure/variables.tf @@ -271,6 +271,17 @@ variable "root_parent_management_group_id" { type = string } +variable "move_subscriptions_to_target_management_group" { + description = <<-EOT + **(Optional, default: `true`)** Controls whether to move target subscriptions under the intermediate root management group. + + When enabled, subscriptions listed in `target_subscriptions` are moved under the created intermediate root management group. + Ensures all landing zone subscriptions are organized under the same management group hierarchy. + EOT + type = bool + default = true +} + variable "intermediate_root_management_group_creation_enabled" { description = <<-EOT **(Optional, default: `true`)** Controls whether to create an intermediate root management group under the root parent. From 7eb8d874aba6253b18c90852ad3fff16a794f809 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Thu, 15 Jan 2026 16:05:54 +0000 Subject: [PATCH 11/42] fmt --- alz/github/variables.tf | 4 ++-- alz/local/main.tf | 2 +- alz/local/variables.tf | 4 ++-- .../locals.intermediate_root_management_group.tf | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/alz/github/variables.tf b/alz/github/variables.tf index 11dd9fca..ef90d395 100644 --- a/alz/github/variables.tf +++ b/alz/github/variables.tf @@ -879,7 +879,7 @@ variable "role_assignments_bicep" { })) default = { plan = { - built_in_role_definition_name = "Reader" + built_in_role_definition_name = "Reader" user_assigned_managed_identity_key = "plan" scope = "management_group" } @@ -889,7 +889,7 @@ variable "role_assignments_bicep" { scope = "management_group" } apply_management_group = { - built_in_role_definition_name = "Owner" + built_in_role_definition_name = "Owner" user_assigned_managed_identity_key = "apply" scope = "management_group" } diff --git a/alz/local/main.tf b/alz/local/main.tf index 9812d521..97727240 100644 --- a/alz/local/main.tf +++ b/alz/local/main.tf @@ -45,7 +45,7 @@ module "azure" { intermediate_root_management_group_creation_enabled = var.iac_type != "bicep-classic" intermediate_root_management_group_id = module.file_manipulation.intermediate_root_management_group_id intermediate_root_management_group_display_name = module.file_manipulation.intermediate_root_management_group_display_name - move_subscriptions_to_target_management_group = var.iac_type != "bicep-classic" + move_subscriptions_to_target_management_group = var.iac_type != "bicep-classic" } module "file_manipulation" { diff --git a/alz/local/variables.tf b/alz/local/variables.tf index 12eddf9c..3aca01a0 100644 --- a/alz/local/variables.tf +++ b/alz/local/variables.tf @@ -592,7 +592,7 @@ variable "role_assignments_bicep" { })) default = { plan = { - built_in_role_definition_name = "Reader" + built_in_role_definition_name = "Reader" user_assigned_managed_identity_key = "plan" scope = "management_group" } @@ -602,7 +602,7 @@ variable "role_assignments_bicep" { scope = "management_group" } apply_management_group = { - built_in_role_definition_name = "Owner" + built_in_role_definition_name = "Owner" user_assigned_managed_identity_key = "apply" scope = "management_group" } diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf index c9d18b47..fbb439e2 100644 --- a/modules/file_manipulation/locals.intermediate_root_management_group.tf +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -1,7 +1,7 @@ locals { is_terraform_iac_type = var.iac_type == "terraform" terraform_architecture = local.is_terraform_iac_type ? endswith(var.terraform_architecture_file_path, ".yaml") || endswith(var.terraform_architecture_file_path, ".json") ? yamldecode(file("${var.module_folder_path}/${var.terraform_architecture_file_path}")) : jsondecode(file("${var.module_folder_path}/${var.terraform_architecture_file_path}")) : null - terraform_intermediate_root_management_group = local.is_terraform_iac_type ? ([ for management_group in local.terraform_architecture.management_groups : management_group if management_group.parent_id == null ])[0] : null + terraform_intermediate_root_management_group = local.is_terraform_iac_type ? ([for management_group in local.terraform_architecture.management_groups : management_group if management_group.parent_id == null])[0] : null intermediate_root_management_group = local.is_terraform_iac_type ? { id = local.terraform_intermediate_root_management_group.id display_name = local.terraform_intermediate_root_management_group.display_name From 358a8842279fbbf97db156cd6116f7866b8f9032 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Thu, 15 Jan 2026 17:40:05 +0000 Subject: [PATCH 12/42] bug fixes --- alz/azuredevops/main.tf | 45 +++++++++--------- alz/azuredevops/variables.tf | 11 +++++ alz/github/main.tf | 47 ++++++++++--------- alz/github/variables.tf | 12 +++++ alz/local/main.tf | 31 ++++++------ alz/local/variables.tf | 12 +++++ ...cals.intermediate_root_management_group.tf | 2 +- modules/file_manipulation/variables.tf | 10 ++++ 8 files changed, 109 insertions(+), 61 deletions(-) diff --git a/alz/azuredevops/main.tf b/alz/azuredevops/main.tf index 48819723..da68bd37 100644 --- a/alz/azuredevops/main.tf +++ b/alz/azuredevops/main.tf @@ -103,26 +103,27 @@ module "azure_devops" { } module "file_manipulation" { - source = "../../modules/file_manipulation" - vcs_type = "azuredevops" - files = module.files.files - use_self_hosted_agents_runners = var.use_self_hosted_agents - resource_names = local.resource_names - use_separate_repository_for_templates = var.use_separate_repository_for_templates - iac_type = var.iac_type - module_folder_path = local.starter_module_folder_path - bicep_config_file_path = var.bicep_config_file_path - starter_module_name = var.starter_module_name - project_or_organization_name = var.azure_devops_project_name - root_module_folder_relative_path = var.root_module_folder_relative_path - on_demand_folder_repository = var.on_demand_folder_repository - on_demand_folder_artifact_name = var.on_demand_folder_artifact_name - ci_template_file_name = local.ci_template_file_name - cd_template_file_name = local.cd_template_file_name - pipeline_target_folder_name = local.target_folder_name - bicep_parameters_file_path = var.bicep_parameters_file_path - agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration - pipeline_files_directory_path = local.pipeline_files_directory_path - pipeline_template_files_directory_path = local.pipeline_template_files_directory_path - terraform_architecture_file_path = var.terraform_architecture_file_path + source = "../../modules/file_manipulation" + vcs_type = "azuredevops" + files = module.files.files + use_self_hosted_agents_runners = var.use_self_hosted_agents + resource_names = local.resource_names + use_separate_repository_for_templates = var.use_separate_repository_for_templates + iac_type = var.iac_type + module_folder_path = local.starter_module_folder_path + bicep_config_file_path = var.bicep_config_file_path + starter_module_name = var.starter_module_name + project_or_organization_name = var.azure_devops_project_name + root_module_folder_relative_path = var.root_module_folder_relative_path + on_demand_folder_repository = var.on_demand_folder_repository + on_demand_folder_artifact_name = var.on_demand_folder_artifact_name + ci_template_file_name = local.ci_template_file_name + cd_template_file_name = local.cd_template_file_name + pipeline_target_folder_name = local.target_folder_name + bicep_parameters_file_path = var.bicep_parameters_file_path + agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration + pipeline_files_directory_path = local.pipeline_files_directory_path + pipeline_template_files_directory_path = local.pipeline_template_files_directory_path + terraform_architecture_file_path = var.terraform_architecture_file_path + terraform_intermediate_root_management_group_state_resource_path_for_import = var.terraform_intermediate_root_management_group_state_resource_path_for_import } \ No newline at end of file diff --git a/alz/azuredevops/variables.tf b/alz/azuredevops/variables.tf index c7731217..3767c53c 100644 --- a/alz/azuredevops/variables.tf +++ b/alz/azuredevops/variables.tf @@ -989,3 +989,14 @@ variable "terraform_architecture_file_path" { type = string default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" } + +variable "terraform_intermediate_root_management_group_state_resource_path_for_import" { + description = <<-EOT + **(Optional, default: `null`)** Resource path for the management group in the Terraform architecture. + + Used for generating accurate resource references in Terraform deployments. + Null when not applicable. + EOT + type = string + default = "module.management_groups[0].module.management_groups.azapi_resource.management_groups_level_0" +} diff --git a/alz/github/main.tf b/alz/github/main.tf index 2249a74a..bcda742b 100644 --- a/alz/github/main.tf +++ b/alz/github/main.tf @@ -104,27 +104,28 @@ module "github" { } module "file_manipulation" { - source = "../../modules/file_manipulation" - vcs_type = "github" - files = module.files.files - use_self_hosted_agents_runners = var.use_self_hosted_runners - resource_names = local.resource_names - use_separate_repository_for_templates = var.use_separate_repository_for_templates - iac_type = var.iac_type - module_folder_path = local.starter_module_folder_path - bicep_config_file_path = var.bicep_config_file_path - starter_module_name = var.starter_module_name - project_or_organization_name = var.github_organization_name - root_module_folder_relative_path = var.root_module_folder_relative_path - on_demand_folder_repository = var.on_demand_folder_repository - on_demand_folder_artifact_name = var.on_demand_folder_artifact_name - ci_template_file_name = local.ci_template_file_name - cd_template_file_name = local.cd_template_file_name - pipeline_target_folder_name = local.target_folder_name - bicep_parameters_file_path = var.bicep_parameters_file_path - agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration - pipeline_files_directory_path = local.pipeline_files_directory_path - pipeline_template_files_directory_path = local.pipeline_template_files_directory_path - concurrency_value = local.resource_names.storage_container - terraform_architecture_file_path = var.terraform_architecture_file_path + source = "../../modules/file_manipulation" + vcs_type = "github" + files = module.files.files + use_self_hosted_agents_runners = var.use_self_hosted_runners + resource_names = local.resource_names + use_separate_repository_for_templates = var.use_separate_repository_for_templates + iac_type = var.iac_type + module_folder_path = local.starter_module_folder_path + bicep_config_file_path = var.bicep_config_file_path + starter_module_name = var.starter_module_name + project_or_organization_name = var.github_organization_name + root_module_folder_relative_path = var.root_module_folder_relative_path + on_demand_folder_repository = var.on_demand_folder_repository + on_demand_folder_artifact_name = var.on_demand_folder_artifact_name + ci_template_file_name = local.ci_template_file_name + cd_template_file_name = local.cd_template_file_name + pipeline_target_folder_name = local.target_folder_name + bicep_parameters_file_path = var.bicep_parameters_file_path + agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration + pipeline_files_directory_path = local.pipeline_files_directory_path + pipeline_template_files_directory_path = local.pipeline_template_files_directory_path + concurrency_value = local.resource_names.storage_container + terraform_architecture_file_path = var.terraform_architecture_file_path + terraform_intermediate_root_management_group_state_resource_path_for_import = var.terraform_intermediate_root_management_group_state_resource_path_for_import } diff --git a/alz/github/variables.tf b/alz/github/variables.tf index ef90d395..484ceaa9 100644 --- a/alz/github/variables.tf +++ b/alz/github/variables.tf @@ -1041,3 +1041,15 @@ variable "terraform_architecture_file_path" { type = string default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" } + +variable "terraform_intermediate_root_management_group_state_resource_path_for_import" { + description = <<-EOT + **(Optional, default: `null`)** Resource path for the management group in the Terraform architecture. + + Used for generating accurate resource references in Terraform deployments. + Null when not applicable. + EOT + type = string + default = "module.management_groups[0].module.management_groups.azapi_resource.management_groups_level_0" +} + diff --git a/alz/local/main.tf b/alz/local/main.tf index 97727240..ba7a9eac 100644 --- a/alz/local/main.tf +++ b/alz/local/main.tf @@ -49,21 +49,22 @@ module "azure" { } module "file_manipulation" { - source = "../../modules/file_manipulation" - vcs_type = "local" - files = module.files.files - resource_names = local.resource_names - iac_type = var.iac_type - module_folder_path = local.starter_module_folder_path - bicep_config_file_path = var.bicep_config_file_path - starter_module_name = var.starter_module_name - root_module_folder_relative_path = var.root_module_folder_relative_path - on_demand_folder_repository = var.on_demand_folder_repository - on_demand_folder_artifact_name = var.on_demand_folder_artifact_name - pipeline_target_folder_name = local.script_target_folder_name - bicep_parameters_file_path = var.bicep_parameters_file_path - pipeline_files_directory_path = local.script_source_folder_path - terraform_architecture_file_path = var.terraform_architecture_file_path + source = "../../modules/file_manipulation" + vcs_type = "local" + files = module.files.files + resource_names = local.resource_names + iac_type = var.iac_type + module_folder_path = local.starter_module_folder_path + bicep_config_file_path = var.bicep_config_file_path + starter_module_name = var.starter_module_name + root_module_folder_relative_path = var.root_module_folder_relative_path + on_demand_folder_repository = var.on_demand_folder_repository + on_demand_folder_artifact_name = var.on_demand_folder_artifact_name + pipeline_target_folder_name = local.script_target_folder_name + bicep_parameters_file_path = var.bicep_parameters_file_path + pipeline_files_directory_path = local.script_source_folder_path + terraform_architecture_file_path = var.terraform_architecture_file_path + terraform_intermediate_root_management_group_state_resource_path_for_import = var.terraform_intermediate_root_management_group_state_resource_path_for_import } resource "local_file" "alz" { diff --git a/alz/local/variables.tf b/alz/local/variables.tf index 3aca01a0..f91125ca 100644 --- a/alz/local/variables.tf +++ b/alz/local/variables.tf @@ -740,3 +740,15 @@ variable "terraform_architecture_file_path" { type = string default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" } + +variable "terraform_intermediate_root_management_group_state_resource_path_for_import" { + description = <<-EOT + **(Optional, default: `null`)** Resource path for the management group in the Terraform architecture. + + Used for generating accurate resource references in Terraform deployments. + Null when not applicable. + EOT + type = string + default = "module.management_groups[0].module.management_groups.azapi_resource.management_groups_level_0" +} + diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf index fbb439e2..fcc77c81 100644 --- a/modules/file_manipulation/locals.intermediate_root_management_group.tf +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -14,7 +14,7 @@ locals { locals { import_block = < Date: Fri, 16 Jan 2026 17:14:49 +0000 Subject: [PATCH 13/42] Move exists = true method --- .github/workflows/end-to-end-test.yml | 5 ++- ...cals.intermediate_root_management_group.tf | 35 +++++++++++++------ modules/file_manipulation/outputs.tf | 7 +--- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index fa0763f5..d42e76a8 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -302,8 +302,11 @@ jobs: $Inputs["resource_name_suffix"] = $uniqueId $architectureFilePath = "${{ env.STARTER_MODULE_FOLDER }}/templates/$starterModule/lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" $architectureFile = Get-Content -Path $architectureFilePath -Raw - $architectureFile = $architectureFile.Replace("id: test", "id: test-$uniqueId") + $architectureFile = $architectureFile.Replace("- id: child-test", "- id: child-test-$uniqueId") + $architectureFile = $architectureFile.Replace("display_name: Child Test", "display_name: Child Test $uniqueId") + $architectureFile = $architectureFile.Replace("- id: test", "- id: test-$uniqueId") $architectureFile = $architectureFile.Replace("display_name: Test", "display_name: Test $uniqueId") + $architectureFile = $architectureFile.Replace("parent_id: test", "prefix: $uniqueId") $architectureFile | Out-File -FilePath $architectureFilePath -Encoding utf8 -Force } diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf index fcc77c81..89403f06 100644 --- a/modules/file_manipulation/locals.intermediate_root_management_group.tf +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -1,6 +1,10 @@ +# Get the intermediate root management group from the terraform architecture file or bicep parameters locals { is_terraform_iac_type = var.iac_type == "terraform" - terraform_architecture = local.is_terraform_iac_type ? endswith(var.terraform_architecture_file_path, ".yaml") || endswith(var.terraform_architecture_file_path, ".json") ? yamldecode(file("${var.module_folder_path}/${var.terraform_architecture_file_path}")) : jsondecode(file("${var.module_folder_path}/${var.terraform_architecture_file_path}")) : null + terraform_architecture_file_path = "${var.module_folder_path}/${var.terraform_architecture_file_path}" + terraform_architecture_file_extension = split(".", var.terraform_architecture_file_path)[length(split(".", var.terraform_architecture_file_path)) - 1] + terraform_architecture_file_is_yaml = local.terraform_architecture_file_extension == "yaml" || local.terraform_architecture_file_extension == "yml" + terraform_architecture = local.is_terraform_iac_type ? (local.terraform_architecture_file_is_yaml ? yamldecode(file(local.terraform_architecture_file_path)) : jsondecode(file(local.terraform_architecture_file_path))) : null terraform_intermediate_root_management_group = local.is_terraform_iac_type ? ([for management_group in local.terraform_architecture.management_groups : management_group if management_group.parent_id == null])[0] : null intermediate_root_management_group = local.is_terraform_iac_type ? { id = local.terraform_intermediate_root_management_group.id @@ -11,17 +15,28 @@ locals { } } +# Transform the intermediate root management group in the terraform architecture file to ensure it is marked as existing locals { - import_block = < Date: Thu, 22 Jan 2026 17:36:37 +0000 Subject: [PATCH 14/42] bin redundant role assignments --- modules/azure/storage.tf | 16 ---------------- .../locals.intermediate_root_management_group.tf | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/modules/azure/storage.tf b/modules/azure/storage.tf index e6067500..ab33a4fd 100644 --- a/modules/azure/storage.tf +++ b/modules/azure/storage.tf @@ -71,19 +71,3 @@ resource "azurerm_role_assignment" "alz_storage_container_additional" { role_definition_name = "Storage Blob Data Owner" principal_id = each.value } - -# These role assignments are a temporary addition to handle this issue in the Terraform CLI: https://github.com/hashicorp/terraform/issues/36595 -# They will be removed once the issue has been resolved -resource "azurerm_role_assignment" "alz_storage_reader" { - for_each = var.create_storage_account ? var.user_assigned_managed_identities : {} - scope = azurerm_storage_account.alz[0].id - role_definition_name = "Reader" - principal_id = azurerm_user_assigned_identity.alz[each.key].principal_id -} - -resource "azurerm_role_assignment" "alz_storage_reader_additional" { - for_each = var.create_storage_account ? var.additional_role_assignment_principal_ids : {} - scope = azurerm_storage_account.alz[0].id - role_definition_name = "Reader" - principal_id = each.value -} diff --git a/modules/file_manipulation/locals.intermediate_root_management_group.tf b/modules/file_manipulation/locals.intermediate_root_management_group.tf index 89403f06..90b35717 100644 --- a/modules/file_manipulation/locals.intermediate_root_management_group.tf +++ b/modules/file_manipulation/locals.intermediate_root_management_group.tf @@ -17,7 +17,7 @@ locals { # Transform the intermediate root management group in the terraform architecture file to ensure it is marked as existing locals { - terraform_management_groups_non_root = local.is_terraform_iac_type ? [ for management_group in local.terraform_architecture.management_groups : management_group if management_group.parent_id != null ] : null + terraform_management_groups_non_root = local.is_terraform_iac_type ? [for management_group in local.terraform_architecture.management_groups : management_group if management_group.parent_id != null] : null terraform_intermediate_root_management_group_updated = local.is_terraform_iac_type ? merge( local.terraform_intermediate_root_management_group, { From 4e74d99f8f8ba0f3223de75fdb8f8539f9a93d79 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 23 Jan 2026 13:27:16 +0000 Subject: [PATCH 15/42] fix ternary --- modules/azure/role_assignments.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/azure/role_assignments.tf b/modules/azure/role_assignments.tf index 670ee8f8..6eeac2c6 100644 --- a/modules/azure/role_assignments.tf +++ b/modules/azure/role_assignments.tf @@ -32,7 +32,7 @@ locals { for subscription_id, subscription in data.azurerm_subscription.alz : { key = "${value.user_assigned_managed_identity_key}-${value.custom_role_definition_key}-${subscription_id}" scope = subscription.id - role_definition_id = value.built_in_role_definition_name ? "${subscription.id}${azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id}" : null + role_definition_id = value.built_in_role_definition_name == null ? "${subscription.id}${azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id}" : null role_definition_name = value.built_in_role_definition_name principal_id = value.principal_id } From f13ae1e9533f10675bae185c40a0d7a644e48894 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 23 Jan 2026 13:39:34 +0000 Subject: [PATCH 16/42] bit of debugging --- .github/workflows/end-to-end-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index d42e76a8..7e94b89e 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -332,6 +332,9 @@ jobs: $json = ConvertTo-Json $Inputs -Depth 100 $json | Out-File -FilePath inputs.json -Encoding utf8 -Force + Write-Host "Inputs File Content:" + Write-Host $json + shell: pwsh - name: Run ALZ PowerShell From 615f16a21aade3881e4331adb5b21457fc4f1d31 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 23 Jan 2026 13:41:15 +0000 Subject: [PATCH 17/42] more debugging --- .github/workflows/end-to-end-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 7e94b89e..f57d33da 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -308,6 +308,8 @@ jobs: $architectureFile = $architectureFile.Replace("display_name: Test", "display_name: Test $uniqueId") $architectureFile = $architectureFile.Replace("parent_id: test", "prefix: $uniqueId") $architectureFile | Out-File -FilePath $architectureFilePath -Encoding utf8 -Force + Write-Host "Modified Architecture File Content:" + Write-Host $architectureFile } # Bicep Classic From 19745f54c3daa6b1d095f9b04d72adcf2848e28b Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 23 Jan 2026 13:56:24 +0000 Subject: [PATCH 18/42] fix test --- .github/workflows/end-to-end-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index f57d33da..9b1ecd27 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -324,7 +324,8 @@ jobs: # Bicep if($infrastructureAsCode -eq "bicep") { $Inputs["network_type"] = "none" - $Inputs["intermediate_root_management_group_id"] = "alz-$uniqueId" + $Inputs["management_group_int_root_id"] = "alz-$uniqueId" + $Inputs["management_group_int_root_name"] = "alz-$uniqueId" $Inputs["management_group_id_prefix"] = "" $Inputs["management_group_id_postfix"] = "" $Inputs["management_group_name_prefix"] = "" From e498b37e3cfcd2725bb3db51732cb504aa9815ff Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 23 Jan 2026 14:10:23 +0000 Subject: [PATCH 19/42] bug in role assignment --- modules/azure/role_assignments.tf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/azure/role_assignments.tf b/modules/azure/role_assignments.tf index 6eeac2c6..16a55498 100644 --- a/modules/azure/role_assignments.tf +++ b/modules/azure/role_assignments.tf @@ -38,9 +38,10 @@ locals { } ] if value.scope == "subscription" ]) : assignment.key => { - scope = assignment.scope - role_definition_id = assignment.role_definition_id - principal_id = assignment.principal_id + scope = assignment.scope + role_definition_id = assignment.role_definition_id + role_definition_name = assignment.role_definition_name + principal_id = assignment.principal_id } } management_group_role_assignments = { From 48ca0c8306d1a0d8576b43ced4110f5f10f9e644 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 23 Jan 2026 15:15:59 +0000 Subject: [PATCH 20/42] fix test --- .github/workflows/end-to-end-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 9b1ecd27..606a1bac 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -306,7 +306,7 @@ jobs: $architectureFile = $architectureFile.Replace("display_name: Child Test", "display_name: Child Test $uniqueId") $architectureFile = $architectureFile.Replace("- id: test", "- id: test-$uniqueId") $architectureFile = $architectureFile.Replace("display_name: Test", "display_name: Test $uniqueId") - $architectureFile = $architectureFile.Replace("parent_id: test", "prefix: $uniqueId") + $architectureFile = $architectureFile.Replace("parent_id: test", "parent_id: test-$uniqueId") $architectureFile | Out-File -FilePath $architectureFilePath -Encoding utf8 -Force Write-Host "Modified Architecture File Content:" Write-Host $architectureFile From b2bd82bf77753ba0f551c11b541d7932ede7d570 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 23 Jan 2026 16:30:39 +0000 Subject: [PATCH 21/42] fix first run check --- .../helpers/bicep-first-deployment-check.yaml | 15 ++++++++++----- .../bicep-first-deployment-check/action.yaml | 17 +++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/alz/azuredevops/pipelines/bicep/templates/helpers/bicep-first-deployment-check.yaml b/alz/azuredevops/pipelines/bicep/templates/helpers/bicep-first-deployment-check.yaml index c86eea76..18432efe 100644 --- a/alz/azuredevops/pipelines/bicep/templates/helpers/bicep-first-deployment-check.yaml +++ b/alz/azuredevops/pipelines/bicep/templates/helpers/bicep-first-deployment-check.yaml @@ -14,16 +14,21 @@ steps: Inline: | $intRootMgId = "$(MANAGEMENT_GROUP_ID_PREFIX)$(INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID)$(MANAGEMENT_GROUP_ID_POSTFIX)" - $managementGroups = Get-AzManagementGroup - $intRootMg = $managementGroups | Where-Object { $_.Name -eq $intRootMgId } + $managementGroup = Get-AzManagementGroup -GroupName $intRootMgId -Expand $firstDeployment = $true - if($intRootMg -eq $null) { + if($managementGroup -eq $null) { Write-Warning "Cannot find the $intRootMgId Management Group, so assuming this is the first deployment." } else { - Write-Host "Found the $intRootMgId Management Group, so assuming this is not the first deployment." - $firstDeployment = $false + Write-Host "Found the $intRootMgId Management Group." + $children = $managementGroup.Children | Where-Object { $_.Type -eq "Microsoft.Management/managementGroups" } + if($children.Count -gt 0) { + Write-Host "The $intRootMgId Management Group has child management groups, so this is NOT the first deployment." + $firstDeployment = $false + } else { + Write-Host "The $intRootMgId Management Group has NO child management groups, so assuming this is the first deployment." + } } Write-Host "##vso[task.setvariable variable=FIRST_DEPLOYMENT;]$firstDeployment" diff --git a/alz/github/actions/bicep/templates/actions/bicep-first-deployment-check/action.yaml b/alz/github/actions/bicep/templates/actions/bicep-first-deployment-check/action.yaml index a247b14d..adc9743b 100644 --- a/alz/github/actions/bicep/templates/actions/bicep-first-deployment-check/action.yaml +++ b/alz/github/actions/bicep/templates/actions/bicep-first-deployment-check/action.yaml @@ -17,16 +17,21 @@ runs: inlineScript: | $intRootMgId = $env:MANAGEMENT_GROUP_ID_PREFIX + $env:INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID + $env:MANAGEMENT_GROUP_ID_POSTFIX - $managementGroups = Get-AzManagementGroup - $intRootMg = $managementGroups | Where-Object { $_.Name -eq $intRootMgId } + $managementGroup = Get-AzManagementGroup -GroupName $intRootMgId -Expand $firstDeployment = $true - if($intRootMg -eq $null) { - Write-Warning "Cannot find the $intRootMgId Management Group, so assuming this is the first deployment. We must skip checking some deployments since their dependent resources do not exist yet." + if($managementGroup -eq $null) { + Write-Warning "Cannot find the $intRootMgId Management Group, so assuming this is the first deployment." } else { - Write-Host "Found the $intRootMgId Management Group, so assuming this is not the first deployment." - $firstDeployment = $false + Write-Host "Found the $intRootMgId Management Group." + $children = $managementGroup.Children | Where-Object { $_.Type -eq "Microsoft.Management/managementGroups" } + if($children.Count -gt 0) { + Write-Host "The $intRootMgId Management Group has child management groups, so this is NOT the first deployment." + $firstDeployment = $false + } else { + Write-Host "The $intRootMgId Management Group has NO child management groups, so assuming this is the first deployment." + } } echo "firstDeployment=$firstDeployment" >> $env:GITHUB_ENV env: From e08e85b4c358fcd898f981d851e2dd4cdd8a00f4 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 23 Jan 2026 18:23:05 +0000 Subject: [PATCH 22/42] fix composite keys and bicep mg --- .../bicep/templates/cd-template.yaml | 2 -- .../bicep/templates/ci-template.yaml | 1 - .../bicep/templates/helpers/bicep-deploy.yaml | 26 +++++------------- .../actions/bicep-deploy/action.yaml | 27 +++++-------------- .../templates/workflows/cd-template.yaml | 2 -- .../templates/workflows/ci-template.yaml | 1 - alz/local/scripts-bicep/bicep-deploy.ps1 | 16 +++++------ alz/local/scripts-bicep/deploy-local.ps1 | 1 - modules/azure/role_assignments.tf | 6 ++--- modules/file_manipulation/locals.bicep.tf | 1 - 10 files changed, 23 insertions(+), 60 deletions(-) diff --git a/alz/azuredevops/pipelines/bicep/templates/cd-template.yaml b/alz/azuredevops/pipelines/bicep/templates/cd-template.yaml index 22196c20..b1bb4601 100644 --- a/alz/azuredevops/pipelines/bicep/templates/cd-template.yaml +++ b/alz/azuredevops/pipelines/bicep/templates/cd-template.yaml @@ -67,7 +67,6 @@ stages: serviceConnection: '${service_connection_name_plan}' templateFilePath: '${script_file.templateFilePath}' templateParametersFilePath: '${script_file.templateParametersFilePath}' - managementGroupId: '${script_file.managementGroupIdVariable}' subscriptionId: '${script_file.subscriptionIdVariable}' resourceGroupName: '${script_file.resourceGroupNameVariable}' location: '$(LOCATION)' @@ -128,7 +127,6 @@ stages: serviceConnection: '${service_connection_name_apply}' templateFilePath: '${script_file.templateFilePath}' templateParametersFilePath: '${script_file.templateParametersFilePath}' - managementGroupId: '${script_file.managementGroupIdVariable}' subscriptionId: '${script_file.subscriptionIdVariable}' resourceGroupName: '${script_file.resourceGroupNameVariable}' location: '$(LOCATION)' diff --git a/alz/azuredevops/pipelines/bicep/templates/ci-template.yaml b/alz/azuredevops/pipelines/bicep/templates/ci-template.yaml index 7a2225d0..58c5cf1f 100644 --- a/alz/azuredevops/pipelines/bicep/templates/ci-template.yaml +++ b/alz/azuredevops/pipelines/bicep/templates/ci-template.yaml @@ -82,7 +82,6 @@ stages: serviceConnection: '${service_connection_name_plan}' templateFilePath: '${script_file.templateFilePath}' templateParametersFilePath: '${script_file.templateParametersFilePath}' - managementGroupId: '${script_file.managementGroupIdVariable}' subscriptionId: '${script_file.subscriptionIdVariable}' resourceGroupName: '${script_file.resourceGroupNameVariable}' location: '$(LOCATION)' diff --git a/alz/azuredevops/pipelines/bicep/templates/helpers/bicep-deploy.yaml b/alz/azuredevops/pipelines/bicep/templates/helpers/bicep-deploy.yaml index ff157441..087c85e1 100644 --- a/alz/azuredevops/pipelines/bicep/templates/helpers/bicep-deploy.yaml +++ b/alz/azuredevops/pipelines/bicep/templates/helpers/bicep-deploy.yaml @@ -10,9 +10,6 @@ parameters: type: string - name: templateParametersFilePath type: string - - name: managementGroupId - type: string - default: '' - name: subscriptionId type: string default: '' @@ -71,7 +68,8 @@ steps: } # Generate deployment stack name - $deploymentPrefix = $env:MANAGEMENT_GROUP_ID_PREFIX + $env:INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID + $env:MANAGEMENT_GROUP_ID_POSTFIX + $intRootMgId = $env:MANAGEMENT_GROUP_ID_PREFIX + $env:INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID + $env:MANAGEMENT_GROUP_ID_POSTFIX + $deploymentPrefix = $intRootMgId $deploymentNameBase = "$${{ parameters.name }}".Replace(" ", "-") $deploymentNameMaxLength = 64 - $deploymentPrefix.Length - 1 if ($deploymentNameBase.Length -gt $deploymentNameMaxLength) { @@ -89,7 +87,7 @@ steps: Write-Host "Deployment Name: $deploymentName" -ForegroundColor DarkGray Write-Host "Template File Path: $${{ parameters.templateFilePath }}" -ForegroundColor DarkGray Write-Host "Template Parameters File Path: $${{ parameters.templateParametersFilePath }}" -ForegroundColor DarkGray - Write-Host "Management Group Id: $${{ parameters.managementGroupId }}" -ForegroundColor DarkGray + Write-Host "Management Group Id: $intRootMgId" -ForegroundColor DarkGray Write-Host "Subscription Id: $${{ parameters.subscriptionId }}" -ForegroundColor DarkGray Write-Host "Resource Group Name: $${{ parameters.resourceGroupName }}" -ForegroundColor DarkGray Write-Host "Location: $${{ parameters.location }}" -ForegroundColor DarkGray @@ -128,14 +126,9 @@ steps: try { switch ($deploymentType) { "managementGroup" { - $targetManagementGroupId = "$${{ parameters.managementGroupId }}" - if ([string]::IsNullOrWhiteSpace($targetManagementGroupId)) { - $targetManagementGroupId = (Get-AzContext).Tenant.TenantId - } - Write-Host "Running Management Group What-If: $deploymentName" -ForegroundColor Cyan $whatIfParameters.Location = "$${{ parameters.location }}" - $whatIfParameters.ManagementGroupId = $targetManagementGroupId + $whatIfParameters.ManagementGroupId = $intRootMgId $result = New-AzManagementGroupDeployment @whatIfParameters } "subscription" { @@ -191,15 +184,10 @@ steps: try { switch ($deploymentType) { "managementGroup" { - $targetManagementGroupId = "$${{ parameters.managementGroupId }}" - if ([string]::IsNullOrWhiteSpace($targetManagementGroupId)) { - $targetManagementGroupId = (Get-AzContext).Tenant.TenantId - } - # Clean up all deployments before each deployment to avoid quota issues try { Write-Host "Cleaning up existing deployments in management group..." -ForegroundColor Cyan - $allDeployments = Get-AzManagementGroupDeployment -ManagementGroupId $targetManagementGroupId -ErrorAction SilentlyContinue + $allDeployments = Get-AzManagementGroupDeployment -ManagementGroupId $intRootMgId -ErrorAction SilentlyContinue if ($allDeployments -and $allDeployments.Count -gt 0) { Write-Host "Found $($allDeployments.Count) deployment(s) to clean up" -ForegroundColor Yellow $batchSize = 200 @@ -207,7 +195,7 @@ steps: $batch = $allDeployments | Select-Object -Skip $i -First $batchSize Write-Host " Deleting batch of $($batch.Count) deployments..." -ForegroundColor Gray $batch | ForEach-Object -Parallel { - Remove-AzManagementGroupDeployment -ManagementGroupId $using:targetManagementGroupId -Name $_.DeploymentName -ErrorAction SilentlyContinue + Remove-AzManagementGroupDeployment -ManagementGroupId $using:intRootMgId -Name $_.DeploymentName -ErrorAction SilentlyContinue } -ThrottleLimit 100 } Write-Host "✓ All deployments cleaned up" -ForegroundColor Green @@ -219,7 +207,7 @@ steps: } Write-Host "Creating Management Group Deployment Stack: $deploymentName" -ForegroundColor Cyan - $result = New-AzManagementGroupDeploymentStack @stackParameters -ManagementGroupId $targetManagementGroupId -Location "$${{ parameters.location }}" + $result = New-AzManagementGroupDeploymentStack @stackParameters -ManagementGroupId $intRootMgId -Location "$${{ parameters.location }}" } "subscription" { if (-not [string]::IsNullOrWhiteSpace("$${{ parameters.subscriptionId }}")) { diff --git a/alz/github/actions/bicep/templates/actions/bicep-deploy/action.yaml b/alz/github/actions/bicep/templates/actions/bicep-deploy/action.yaml index 2a29d907..22b85b58 100644 --- a/alz/github/actions/bicep/templates/actions/bicep-deploy/action.yaml +++ b/alz/github/actions/bicep/templates/actions/bicep-deploy/action.yaml @@ -14,9 +14,6 @@ inputs: templateParametersFilePath: description: 'The path to the parameters file' required: true - managementGroupId: - description: 'The root parent management group id' - required: true subscriptionId: description: 'The subscription id' required: true @@ -66,7 +63,8 @@ runs: } # Generate deployment stack name - $deploymentPrefix = $env:MANAGEMENT_GROUP_ID_PREFIX + $env:INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID + $env:MANAGEMENT_GROUP_ID_POSTFIX + $intRootMgId = $env:MANAGEMENT_GROUP_ID_PREFIX + $env:INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID + $env:MANAGEMENT_GROUP_ID_POSTFIX + $deploymentPrefix = $intRootMgId $deploymentNameBase = ($env:NAME).Replace(" ", "-") $deploymentNameMaxLength = 64 - $deploymentPrefix.Length - 1 if ($deploymentNameBase.Length -gt $deploymentNameMaxLength) { @@ -84,7 +82,7 @@ runs: Write-Host "Deployment Name: $deploymentName" -ForegroundColor DarkGray Write-Host "Template File Path: $env:TEMPLATE_FILE_PATH" -ForegroundColor DarkGray Write-Host "Template Parameters File Path: $env:TEMPLATE_PARAMETERS_FILE_PATH" -ForegroundColor DarkGray - Write-Host "Management Group Id: $env:MANAGEMENT_GROUP_ID" -ForegroundColor DarkGray + Write-Host "Management Group Id: $intRootMgId" -ForegroundColor DarkGray Write-Host "Subscription Id: $env:SUBSCRIPTION_ID" -ForegroundColor DarkGray Write-Host "Resource Group Name: $env:RESOURCE_GROUP_NAME" -ForegroundColor DarkGray Write-Host "Location: $env:LOCATION" -ForegroundColor DarkGray @@ -123,14 +121,9 @@ runs: try { switch ($deploymentType) { "managementGroup" { - $targetManagementGroupId = $env:MANAGEMENT_GROUP_ID - if ([string]::IsNullOrWhiteSpace($targetManagementGroupId)) { - $targetManagementGroupId = (Get-AzContext).Tenant.TenantId - } - Write-Host "Running Management Group What-If: $deploymentName" -ForegroundColor Cyan $whatIfParameters.Location = $env:LOCATION - $whatIfParameters.ManagementGroupId = $targetManagementGroupId + $whatIfParameters.ManagementGroupId = $intRootMgId $result = New-AzManagementGroupDeployment @whatIfParameters } "subscription" { @@ -209,15 +202,10 @@ runs: try { switch ($deploymentType) { "managementGroup" { - $targetManagementGroupId = $env:MANAGEMENT_GROUP_ID - if ([string]::IsNullOrWhiteSpace($targetManagementGroupId)) { - $targetManagementGroupId = (Get-AzContext).Tenant.TenantId - } - # Clean up all deployments before each deployment to avoid quota issues try { Write-Host "Cleaning up existing deployments in management group..." -ForegroundColor Cyan - $allDeployments = Get-AzManagementGroupDeployment -ManagementGroupId $targetManagementGroupId -ErrorAction SilentlyContinue + $allDeployments = Get-AzManagementGroupDeployment -ManagementGroupId $intRootMgId -ErrorAction SilentlyContinue if ($allDeployments -and $allDeployments.Count -gt 0) { Write-Host "Found $($allDeployments.Count) deployment(s) to clean up" -ForegroundColor Yellow $batchSize = 200 @@ -225,7 +213,7 @@ runs: $batch = $allDeployments | Select-Object -Skip $i -First $batchSize Write-Host " Deleting batch of $($batch.Count) deployments..." -ForegroundColor Gray $batch | ForEach-Object -Parallel { - Remove-AzManagementGroupDeployment -ManagementGroupId $using:targetManagementGroupId -Name $_.DeploymentName -ErrorAction SilentlyContinue + Remove-AzManagementGroupDeployment -ManagementGroupId $using:intRootMgId -Name $_.DeploymentName -ErrorAction SilentlyContinue } -ThrottleLimit 100 } Write-Host "✓ All deployments cleaned up" -ForegroundColor Green @@ -237,7 +225,7 @@ runs: } Write-Host "Creating Management Group Deployment Stack: $deploymentName" -ForegroundColor Cyan - $result = New-AzManagementGroupDeploymentStack @stackParameters -ManagementGroupId $targetManagementGroupId -Location $env:LOCATION + $result = New-AzManagementGroupDeploymentStack @stackParameters -ManagementGroupId $intRootMgId -Location $env:LOCATION } "subscription" { if (-not [string]::IsNullOrWhiteSpace($env:SUBSCRIPTION_ID)) { @@ -340,7 +328,6 @@ runs: DISPLAY_NAME: $${{ inputs.displayName }} TEMPLATE_FILE_PATH: $${{ inputs.templateFilePath }} TEMPLATE_PARAMETERS_FILE_PATH: $${{ inputs.templateParametersFilePath }} - MANAGEMENT_GROUP_ID: $${{ inputs.managementGroupId }} SUBSCRIPTION_ID: $${{ inputs.subscriptionId }} RESOURCE_GROUP_NAME: $${{ inputs.resourceGroupName }} LOCATION: $${{ inputs.location }} diff --git a/alz/github/actions/bicep/templates/workflows/cd-template.yaml b/alz/github/actions/bicep/templates/workflows/cd-template.yaml index a3b1ccfb..302261ff 100644 --- a/alz/github/actions/bicep/templates/workflows/cd-template.yaml +++ b/alz/github/actions/bicep/templates/workflows/cd-template.yaml @@ -65,7 +65,6 @@ jobs: displayName: '${script_file.displayName}' templateFilePath: '${script_file.templateFilePath}' templateParametersFilePath: '${script_file.templateParametersFilePath}' - managementGroupId: '${script_file.managementGroupIdVariable}' subscriptionId: '${script_file.subscriptionIdVariable}' resourceGroupName: '${script_file.resourceGroupNameVariable}' location: '$${{ env.LOCATION }}' @@ -123,7 +122,6 @@ jobs: displayName: '${script_file.displayName}' templateFilePath: '${script_file.templateFilePath}' templateParametersFilePath: '${script_file.templateParametersFilePath}' - managementGroupId: '${script_file.managementGroupIdVariable}' subscriptionId: '${script_file.subscriptionIdVariable}' resourceGroupName: '${script_file.resourceGroupNameVariable}' location: '$${{ env.LOCATION }}' diff --git a/alz/github/actions/bicep/templates/workflows/ci-template.yaml b/alz/github/actions/bicep/templates/workflows/ci-template.yaml index d4975046..bc438654 100644 --- a/alz/github/actions/bicep/templates/workflows/ci-template.yaml +++ b/alz/github/actions/bicep/templates/workflows/ci-template.yaml @@ -85,7 +85,6 @@ jobs: displayName: '${script_file.displayName}' templateFilePath: '${script_file.templateFilePath}' templateParametersFilePath: '${script_file.templateParametersFilePath}' - managementGroupId: '${script_file.managementGroupIdVariable}' subscriptionId: '${script_file.subscriptionIdVariable}' resourceGroupName: '${script_file.resourceGroupNameVariable}' location: '$${{ env.LOCATION }}' diff --git a/alz/local/scripts-bicep/bicep-deploy.ps1 b/alz/local/scripts-bicep/bicep-deploy.ps1 index cae976fa..b36c9e38 100644 --- a/alz/local/scripts-bicep/bicep-deploy.ps1 +++ b/alz/local/scripts-bicep/bicep-deploy.ps1 @@ -3,7 +3,6 @@ param( [string]$displayName, [string]$templateFilePath, [string]$templateParametersFilePath, - [string]$managementGroupId, [string]$subscriptionId, [string]$resourceGroupName, [string]$location, @@ -16,6 +15,8 @@ $templateRoot = Split-Path -Parent $scriptRoot $templateFilePath = Join-Path $templateRoot $templateFilePath $templateParametersFilePath = Join-Path $templateRoot $templateParametersFilePath +$intRootMgId = $env:MANAGEMENT_GROUP_ID_PREFIX + $env:INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID + $env:MANAGEMENT_GROUP_ID_POSTFIX + Write-Host "<---------------------------------------------------------------------------->" -ForegroundColor Blue Write-Host "Starting deployment stack for $displayName..." -ForegroundColor Blue Write-Host "<---------------------------------------------------------------------------->" -ForegroundColor Blue @@ -24,7 +25,7 @@ Write-Host "" Write-Host "Display Name: $displayName" -ForegroundColor DarkGray Write-Host "Template File Path: $templateFilePath" -ForegroundColor DarkGray Write-Host "Template Parameters File Path: $templateParametersFilePath" -ForegroundColor DarkGray -Write-Host "Management Group Id: $managementGroupId" -ForegroundColor DarkGray +Write-Host "Management Group Id: $intRootMgId" -ForegroundColor DarkGray Write-Host "Subscription Id: $subscriptionId" -ForegroundColor DarkGray Write-Host "Resource Group Name: $resourceGroupName" -ForegroundColor DarkGray Write-Host "Location: $location" -ForegroundColor DarkGray @@ -85,15 +86,10 @@ while ($retryCount -lt $retryMax) { try { switch ($deploymentType) { "managementGroup" { - $targetManagementGroupId = $managementGroupId - if ([string]::IsNullOrWhiteSpace($targetManagementGroupId)) { - $targetManagementGroupId = (Get-AzContext).Tenant.TenantId - } - # Clean up all deployments before each deployment to avoid quota issues try { Write-Host "Cleaning up existing deployments in management group..." -ForegroundColor Cyan - $allDeployments = Get-AzManagementGroupDeployment -ManagementGroupId $targetManagementGroupId -ErrorAction SilentlyContinue + $allDeployments = Get-AzManagementGroupDeployment -ManagementGroupId $intRootMgId -ErrorAction SilentlyContinue if ($allDeployments -and $allDeployments.Count -gt 0) { Write-Host "Found $($allDeployments.Count) deployment(s) to clean up" -ForegroundColor Yellow $batchSize = 200 @@ -101,7 +97,7 @@ while ($retryCount -lt $retryMax) { $batch = $allDeployments | Select-Object -Skip $i -First $batchSize Write-Host " Deleting batch of $($batch.Count) deployments..." -ForegroundColor Gray $batch | ForEach-Object -Parallel { - Remove-AzManagementGroupDeployment -ManagementGroupId $using:targetManagementGroupId -Name $_.DeploymentName -ErrorAction SilentlyContinue + Remove-AzManagementGroupDeployment -ManagementGroupId $using:intRootMgId -Name $_.DeploymentName -ErrorAction SilentlyContinue } -ThrottleLimit 100 } Write-Host "✓ All deployments cleaned up" -ForegroundColor Green @@ -112,7 +108,7 @@ while ($retryCount -lt $retryMax) { Write-Warning "Could not clean up deployments: $($_.Exception.Message)" } - $result = New-AzManagementGroupDeploymentStack @stackParameters -ManagementGroupId $targetManagementGroupId -Location $location -Verbose + $result = New-AzManagementGroupDeploymentStack @stackParameters -ManagementGroupId $intRootMgId -Location $location -Verbose } "subscription" { if (-not [string]::IsNullOrWhiteSpace($subscriptionId)) { diff --git a/alz/local/scripts-bicep/deploy-local.ps1 b/alz/local/scripts-bicep/deploy-local.ps1 index 63eaa8c9..6c59cbfc 100644 --- a/alz/local/scripts-bicep/deploy-local.ps1 +++ b/alz/local/scripts-bicep/deploy-local.ps1 @@ -25,7 +25,6 @@ if ($deployApproved -ne "yes") { -displayName "${script_file.displayName}" ` -templateFilePath "${script_file.templateFilePath}" ` -templateParametersFilePath "${script_file.templateParametersFilePath}" ` - -managementGroupId ${script_file.managementGroupIdVariable} ` -subscriptionId ${script_file.subscriptionIdVariable} ` -resourceGroupName ${script_file.resourceGroupNameVariable} ` -location $env:LOCATION ` diff --git a/modules/azure/role_assignments.tf b/modules/azure/role_assignments.tf index 16a55498..d0bbc87a 100644 --- a/modules/azure/role_assignments.tf +++ b/modules/azure/role_assignments.tf @@ -10,8 +10,8 @@ locals { additional_role_assignments = { for assignment in flatten([ for key, value in var.role_assignments : [ for princial_key, principal_value in var.additional_role_assignment_principal_ids : { - composite_key = "${value.scope}-${value.custom_role_definition_key}-${princial_key}" - user_assigned_managed_identity_key = "${value.scope}-${value.custom_role_definition_key}-${princial_key}" + composite_key = "${value.scope}-${coalesce(value.custom_role_definition_key, value.built_in_role_definition_name)}-${princial_key}" + user_assigned_managed_identity_key = "${value.scope}-${coalesce(value.custom_role_definition_key, value.built_in_role_definition_name)}-${princial_key}" built_in_role_definition_name = value.built_in_role_definition_name custom_role_definition_key = value.custom_role_definition_key scope = value.scope @@ -30,7 +30,7 @@ locals { subscription_role_assignments = { for assignment in flatten([ for key, value in local.combined_role_assignments : [ for subscription_id, subscription in data.azurerm_subscription.alz : { - key = "${value.user_assigned_managed_identity_key}-${value.custom_role_definition_key}-${subscription_id}" + key = "${value.user_assigned_managed_identity_key}-${coalesce(value.custom_role_definition_key, value.built_in_role_definition_name)}-${subscription_id}" scope = subscription.id role_definition_id = value.built_in_role_definition_name == null ? "${subscription.id}${azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id}" : null role_definition_name = value.built_in_role_definition_name diff --git a/modules/file_manipulation/locals.bicep.tf b/modules/file_manipulation/locals.bicep.tf index 7ebaaf38..c8e74654 100644 --- a/modules/file_manipulation/locals.bicep.tf +++ b/modules/file_manipulation/locals.bicep.tf @@ -49,7 +49,6 @@ locals { displayName = replace(replace(script_file.displayName, "{{unique_postfix}}", var.resource_names.unique_postfix), "{{time_stamp}}", var.resource_names.time_stamp_formatted) templateFilePath = script_file.templateFilePath templateParametersFilePath = script_file.templateParametersFilePath - managementGroupIdVariable = try(format(local.id_variable_template, script_file.managementGroupId), local.id_variable_template_empty) subscriptionIdVariable = try(format(local.id_variable_template, script_file.subscriptionId), local.id_variable_template_empty) resourceGroupNameVariable = try(format(local.id_variable_template, script_file.resourceGroupName), local.id_variable_template_empty) deploymentType = script_file.deploymentType From e817f66b3dbcf058dcd2e9a294c9ac4beafdfcd5 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Tue, 27 Jan 2026 14:07:59 +0000 Subject: [PATCH 23/42] Create 4 subs per test --- .../tests/scripts/create-subscriptions.ps1 | 192 ++++++++++++++++++ .github/tests/scripts/generate-matrix.ps1 | 5 +- .github/workflows/end-to-end-test.yml | 9 +- 3 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 .github/tests/scripts/create-subscriptions.ps1 diff --git a/.github/tests/scripts/create-subscriptions.ps1 b/.github/tests/scripts/create-subscriptions.ps1 new file mode 100644 index 00000000..c1c6cb59 --- /dev/null +++ b/.github/tests/scripts/create-subscriptions.ps1 @@ -0,0 +1,192 @@ +param( + [string]$billingScope, + [string]$subscriptionNamePrefix = "accelerator-bootstrap-modules", + [string[]]$subscriptionTypes = @("connectivity", "management", "identity", "security"), + [int]$maxRetries = 5, + [int]$throttleLimit = 2, + [switch]$planOnly +) + +# Get current Azure account information +$accountInfo = az account show --output json | ConvertFrom-Json + +# Look up tenant name from Graph API domains +$domains = az rest --method get --url "https://graph.microsoft.com/v1.0/domains" --output json | ConvertFrom-Json +$defaultDomain = $domains.value | Where-Object { $_.isDefault -eq $true } +$tenantName = if ($defaultDomain.id) { $defaultDomain.id } else { "(unknown)" } + +Write-Host "" +Write-Host "=== Azure Connection Information ===" -ForegroundColor Cyan +Write-Host "Tenant ID: $($accountInfo.tenantId)" -ForegroundColor Yellow +Write-Host "Tenant Name: $tenantName" -ForegroundColor Yellow +Write-Host "Account: $($accountInfo.user.name)" -ForegroundColor Yellow +Write-Host "Subscription: $($accountInfo.name)" -ForegroundColor Yellow +Write-Host "====================================" -ForegroundColor Cyan +Write-Host "" + +$confirmation = Read-Host "Do you want to continue with this account? (y/n)" +if ($confirmation -ne 'y' -and $confirmation -ne 'Y') { + Write-Host "Operation cancelled by user." -ForegroundColor Red + exit 0 +} + +Write-Host "" + +$tests = ./.github/tests/scripts/generate-matrix.ps1 + +# Get all existing aliases once using REST API with paging (more efficient than checking each one individually) +Write-Host "Fetching existing subscription aliases..." -ForegroundColor Cyan +$existingAliasNames = @() +$aliasUrl = "https://management.azure.com/providers/Microsoft.Subscription/aliases?api-version=2021-10-01" + +do { + $response = az rest --method get --url "`"$aliasUrl`"" | ConvertFrom-Json + if ($response.value) { + $existingAliasNames += $response.value | ForEach-Object { $_.name } + } + $aliasUrl = $response.nextLink +} while ($aliasUrl) + +Write-Host "Fetched $($existingAliasNames.Count) existing aliases." -ForegroundColor Green + +# Build list of subscriptions to create +$subscriptionsToCreate = @() +$existingSubscriptions = @() +$skippedTests = @() + +foreach ($test in $tests) { + # Only create subscriptions for tests that deploy Azure resources + if ($test.deployAzureResources -ne "true") { + $skippedTests += $test.Name + continue + } + + foreach ($subscriptionType in $subscriptionTypes) { + $subscriptionName = "$subscriptionNamePrefix-$($test.ShortNamePrefix)-$subscriptionType" + + if ($existingAliasNames -notcontains $subscriptionName) { + $subscriptionsToCreate += $subscriptionName + } else { + $existingSubscriptions += $subscriptionName + } + } +} + +# Display skipped tests +if ($skippedTests.Count -gt 0) { + Write-Host "" + Write-Host "=== Tests Skipped (deployAzureResources=false) ===" -ForegroundColor Cyan + foreach ($test in $skippedTests) { + Write-Host " - $test" -ForegroundColor Gray + } +} + +# Display existing subscriptions +if ($existingSubscriptions.Count -gt 0) { + Write-Host "" + Write-Host "=== Existing Subscription Aliases (will be skipped) ===" -ForegroundColor Cyan + foreach ($sub in $existingSubscriptions) { + Write-Host " - $sub" -ForegroundColor Gray + } +} + +# Display subscriptions to create +Write-Host "" +if ($subscriptionsToCreate.Count -eq 0) { + Write-Host "No new subscriptions to create. All aliases already exist." -ForegroundColor Green + return +} + +Write-Host "=== Subscriptions to Create ===" -ForegroundColor Cyan +foreach ($sub in $subscriptionsToCreate) { + Write-Host " - $sub" -ForegroundColor Yellow +} +Write-Host "" +Write-Host "Total: $($subscriptionsToCreate.Count) subscription(s) to create" -ForegroundColor Cyan +Write-Host "" + +if ($planOnly) { + Write-Host "Plan only mode - no subscriptions will be created." -ForegroundColor Magenta + return +} + +# Prompt for confirmation before creating +$createConfirmation = Read-Host "Do you want to create these $($subscriptionsToCreate.Count) subscription(s)? (y/n)" +if ($createConfirmation -ne 'y' -and $createConfirmation -ne 'Y') { + Write-Host "Operation cancelled by user." -ForegroundColor Red + return +} + +Write-Host "" + +# Create a thread-safe hashtable to track rate limiting across parallel tasks +$rateLimitState = [hashtable]::Synchronized(@{ + WaitUntil = [DateTime]::MinValue +}) + +# Create the subscriptions in parallel with retry logic +Write-Host "Creating subscriptions (throttle: $throttleLimit)..." -ForegroundColor Cyan + +$results = $subscriptionsToCreate | ForEach-Object -Parallel { + $subscriptionName = $_ + $scope = $using:billingScope + $retries = $using:maxRetries + $state = $using:rateLimitState + $retryCount = 0 + $success = $false + + while (-not $success -and $retryCount -lt $retries) { + # Check if we're in a rate limit wait period + $waitUntil = $state.WaitUntil + if ($waitUntil -gt [DateTime]::Now) { + $waitSeconds = [math]::Ceiling(($waitUntil - [DateTime]::Now).TotalSeconds) + Write-Host "Rate limit active. $subscriptionName waiting $waitSeconds seconds..." -ForegroundColor Yellow + Start-Sleep -Seconds $waitSeconds + } + + Write-Host "Creating subscription: $subscriptionName (Attempt $($retryCount + 1) of $retries)" -ForegroundColor Yellow + $result = az account alias create --name "$subscriptionName" --billing-scope "$scope" --display-name "$subscriptionName" --workload "Production" 2>&1 + + if ($LASTEXITCODE -eq 0) { + $success = $true + Write-Host "Successfully created: $subscriptionName" -ForegroundColor Green + } else { + $errorMessage = $result | Out-String + if ($errorMessage -match "TooManyRequests.*Retry in (\d{2}):(\d{2}):(\d{2})") { + $hours = [int]$Matches[1] + $minutes = [int]$Matches[2] + $seconds = [int]$Matches[3] + $waitSeconds = ($hours * 3600) + ($minutes * 60) + $seconds + 10 # Add 10 seconds buffer + + # Set the shared rate limit wait time + $newWaitUntil = [DateTime]::Now.AddSeconds($waitSeconds) + if ($newWaitUntil -gt $state.WaitUntil) { + $state.WaitUntil = $newWaitUntil + Write-Host "Rate limit hit! All tasks will wait until $($newWaitUntil.ToString('HH:mm:ss'))" -ForegroundColor Red + } + + Write-Host "Rate limited for $subscriptionName. Waiting $waitSeconds seconds before retry..." -ForegroundColor Yellow + Start-Sleep -Seconds $waitSeconds + $retryCount++ + } else { + Write-Host "Failed to create $subscriptionName : $errorMessage" -ForegroundColor Red + break + } + } + } + + [PSCustomObject]@{ + Name = $subscriptionName + Success = $success + } +} -ThrottleLimit $throttleLimit + +$successCount = ($results | Where-Object { $_.Success }).Count +$failCount = ($results | Where-Object { -not $_.Success }).Count + +Write-Host "" +Write-Host "Subscription creation complete." -ForegroundColor Green +Write-Host " Successful: $successCount" -ForegroundColor Green +if ($failCount -gt 0) { + Write-Host " Failed: $failCount" -ForegroundColor Red +} diff --git a/.github/tests/scripts/generate-matrix.ps1 b/.github/tests/scripts/generate-matrix.ps1 index bd67f437..de6c9d6f 100644 --- a/.github/tests/scripts/generate-matrix.ps1 +++ b/.github/tests/scripts/generate-matrix.ps1 @@ -80,7 +80,7 @@ $combinations = [ordered]@{ operatingSystem = @("ubuntu", "windows", "macos") starterModule = @("test") regions = @("multi") - terraformVersion = @("1.5.0") + terraformVersion = @("1.6.0") deployAzureResources = @("false") } local_single_region_tests = [ordered]@{ @@ -164,7 +164,8 @@ function Get-MatrixRecursively { $combination.Name = $name.Trim("-") $combination.Hash = Get-Hash $name - $combination.ShortName = "r" + $combination.Hash.Substring(0, 5).ToLower() + "r" + $runNumber + $combination.ShortNamePrefix = "r" + $combination.Hash.Substring(0, 5).ToLower() + $combination.ShortName = $combination.ShortNamePrefix + "r" + $runNumber $calculatedCombinations += $combination diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 606a1bac..d886c8b7 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -144,6 +144,7 @@ jobs: $regions = "${{ matrix.regions }}" $starterModule = "${{ matrix.starterModule }}" $shortName = "${{ matrix.ShortName }}" + $shortNamePrefix = "${{ matrix.ShortNamePrefix }}" $deployAzureResources = "${{ matrix.deployAzureResources }}" $locations_with_aci_zone_support = @( @@ -280,10 +281,10 @@ jobs: $rootParentManagementGroupId = $selfHostedAgents -eq "none" ? "${{ vars.NESTED_ROOT_PARENT_MANAGEMENT_GROUP_ID }}" : "" $Inputs["root_parent_management_group_id"] = $rootParentManagementGroupId - $subscriptionIDManagement = $selfHostedAgents -eq "none" ? "${{ vars.SUBSCRIPTION_ID_NESTED_MANAGEMENT }}" : "${{ vars.SUBSCRIPTION_ID_ROOT_MANAGEMENT }}" - $subscriptionIDConnectivity = $selfHostedAgents -eq "none" ? "${{ vars.SUBSCRIPTION_ID_NESTED_CONNECTIVITY }}" : "${{ vars.SUBSCRIPTION_ID_ROOT_CONNECTIVITY }}" - $subscriptionIDIdentity = $selfHostedAgents -eq "none" ? "${{ vars.SUBSCRIPTION_ID_NESTED_IDENTITY }}" : "${{ vars.SUBSCRIPTION_ID_ROOT_IDENTITY }}" - $subscriptionIDSecurity = $selfHostedAgents -eq "none" ? "${{ vars.SUBSCRIPTION_ID_NESTED_SECURITY }}" : "${{ vars.SUBSCRIPTION_ID_ROOT_SECURITY }}" + $subscriptionIDManagement = "accelerator-bootstrap-modules-$shortNamePrefix-management" + $subscriptionIDConnectivity = "accelerator-bootstrap-modules-$shortNamePrefix-connectivity" + $subscriptionIDIdentity = "accelerator-bootstrap-modules-$shortNamePrefix-identity" + $subscriptionIDSecurity = "accelerator-bootstrap-modules-$shortNamePrefix-security" $Inputs["subscription_ids"] = @{ management = $subscriptionIDManagement From 6d5807ab958506e9f9ac48b37994b6d897d6d37e Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Tue, 27 Jan 2026 16:39:33 +0000 Subject: [PATCH 24/42] fix write-verbose --- .github/tests/scripts/create-subscriptions.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/tests/scripts/create-subscriptions.ps1 b/.github/tests/scripts/create-subscriptions.ps1 index c1c6cb59..f4d2e27f 100644 --- a/.github/tests/scripts/create-subscriptions.ps1 +++ b/.github/tests/scripts/create-subscriptions.ps1 @@ -1,3 +1,4 @@ +[CmdletBinding()] param( [string]$billingScope, [string]$subscriptionNamePrefix = "accelerator-bootstrap-modules", @@ -25,7 +26,7 @@ Write-Host "====================================" -ForegroundColor Cyan Write-Host "" $confirmation = Read-Host "Do you want to continue with this account? (y/n)" -if ($confirmation -ne 'y' -and $confirmation -ne 'Y') { +if ($confirmation -ine 'y') { Write-Host "Operation cancelled by user." -ForegroundColor Red exit 0 } @@ -112,7 +113,7 @@ if ($planOnly) { # Prompt for confirmation before creating $createConfirmation = Read-Host "Do you want to create these $($subscriptionsToCreate.Count) subscription(s)? (y/n)" -if ($createConfirmation -ne 'y' -and $createConfirmation -ne 'Y') { +if ($createConfirmation -ine 'y') { Write-Host "Operation cancelled by user." -ForegroundColor Red return } @@ -132,6 +133,7 @@ $results = $subscriptionsToCreate | ForEach-Object -Parallel { $scope = $using:billingScope $retries = $using:maxRetries $state = $using:rateLimitState + $VerbosePreference = $using:VerbosePreference $retryCount = 0 $success = $false @@ -156,7 +158,8 @@ $results = $subscriptionsToCreate | ForEach-Object -Parallel { $hours = [int]$Matches[1] $minutes = [int]$Matches[2] $seconds = [int]$Matches[3] - $waitSeconds = ($hours * 3600) + ($minutes * 60) + $seconds + 10 # Add 10 seconds buffer + $waitSeconds = ($hours * 3600) + ($minutes * 60) + $seconds + (1 * 60) # Add 60 second buffer + Write-Verbose $errorMessage # Set the shared rate limit wait time $newWaitUntil = [DateTime]::Now.AddSeconds($waitSeconds) From d3e7d7c9f4b60a06cea6e1b2139301e291c9db7b Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Tue, 27 Jan 2026 16:44:38 +0000 Subject: [PATCH 25/42] re-instate variable needed for classic bicep --- modules/file_manipulation/locals.bicep.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/file_manipulation/locals.bicep.tf b/modules/file_manipulation/locals.bicep.tf index c8e74654..7ebaaf38 100644 --- a/modules/file_manipulation/locals.bicep.tf +++ b/modules/file_manipulation/locals.bicep.tf @@ -49,6 +49,7 @@ locals { displayName = replace(replace(script_file.displayName, "{{unique_postfix}}", var.resource_names.unique_postfix), "{{time_stamp}}", var.resource_names.time_stamp_formatted) templateFilePath = script_file.templateFilePath templateParametersFilePath = script_file.templateParametersFilePath + managementGroupIdVariable = try(format(local.id_variable_template, script_file.managementGroupId), local.id_variable_template_empty) subscriptionIdVariable = try(format(local.id_variable_template, script_file.subscriptionId), local.id_variable_template_empty) resourceGroupNameVariable = try(format(local.id_variable_template, script_file.resourceGroupName), local.id_variable_template_empty) deploymentType = script_file.deploymentType From 9252280541a90b6b33035e7b651faef111ef6a1a Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Tue, 27 Jan 2026 17:29:16 +0000 Subject: [PATCH 26/42] get the subscription id --- .github/workflows/end-to-end-test.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index d886c8b7..a9a40857 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -132,6 +132,13 @@ jobs: terraform_wrapper: false if: ${{ matrix.terraformVersion != 'latest' }} + - name: Azure login + uses: azure/login@v2 + with: + client-id: ${{ vars.ARM_CLIENT_ID }} + tenant-id: ${{ vars.ARM_TENANT_ID }} + subscription-id: ${{ vars.ARM_SUBSCRIPTION_ID }} + - name: Setup ALZ Module Inputs run: | @@ -281,10 +288,10 @@ jobs: $rootParentManagementGroupId = $selfHostedAgents -eq "none" ? "${{ vars.NESTED_ROOT_PARENT_MANAGEMENT_GROUP_ID }}" : "" $Inputs["root_parent_management_group_id"] = $rootParentManagementGroupId - $subscriptionIDManagement = "accelerator-bootstrap-modules-$shortNamePrefix-management" - $subscriptionIDConnectivity = "accelerator-bootstrap-modules-$shortNamePrefix-connectivity" - $subscriptionIDIdentity = "accelerator-bootstrap-modules-$shortNamePrefix-identity" - $subscriptionIDSecurity = "accelerator-bootstrap-modules-$shortNamePrefix-security" + $subscriptionIDManagement = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-management" | ConvertFrom-Json | Select-Object -ExpandProperty id + $subscriptionIDConnectivity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-connectivity" | ConvertFrom-Json | Select-Object -ExpandProperty id + $subscriptionIDIdentity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-identity" | ConvertFrom-Json | Select-Object -ExpandProperty id + $subscriptionIDSecurity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-security" | ConvertFrom-Json | Select-Object -ExpandProperty id $Inputs["subscription_ids"] = @{ management = $subscriptionIDManagement From 8277e0db13f52c21bd1c5a1e76dafaee2f905d47 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 10:52:39 +0000 Subject: [PATCH 27/42] Add bootstrap subs and clean up steps --- .../tests/scripts/create-subscriptions.ps1 | 2 +- .github/workflows/end-to-end-test.yml | 85 +++++++++++++------ 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/.github/tests/scripts/create-subscriptions.ps1 b/.github/tests/scripts/create-subscriptions.ps1 index f4d2e27f..fdab2d12 100644 --- a/.github/tests/scripts/create-subscriptions.ps1 +++ b/.github/tests/scripts/create-subscriptions.ps1 @@ -2,7 +2,7 @@ param( [string]$billingScope, [string]$subscriptionNamePrefix = "accelerator-bootstrap-modules", - [string[]]$subscriptionTypes = @("connectivity", "management", "identity", "security"), + [string[]]$subscriptionTypes = @("connectivity", "management", "identity", "security", "bootstrap"), [int]$maxRetries = 5, [int]$throttleLimit = 2, [switch]$planOnly diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index a9a40857..f4370cd1 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -139,6 +139,32 @@ jobs: tenant-id: ${{ vars.ARM_TENANT_ID }} subscription-id: ${{ vars.ARM_SUBSCRIPTION_ID }} + - name: Get Subscriptions + run: | + $subscriptionIDBootstrap = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-bootstrap" | ConvertFrom-Json | Select-Object -ExpandProperty id + $subscriptionIDManagement = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-management" | ConvertFrom-Json | Select-Object -ExpandProperty id + $subscriptionIDConnectivity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-connectivity" | ConvertFrom-Json | Select-Object -ExpandProperty id + $subscriptionIDIdentity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-identity" | ConvertFrom-Json | Select-Object -ExpandProperty id + $subscriptionIDSecurity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-security" | ConvertFrom-Json | Select-Object -ExpandProperty id + + "ARM_SUBSCRIPTION_ID=$subscriptionIDBootstrap" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_BOOTSTRAP=$subscriptionIDBootstrap" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_MANAGEMENT=$subscriptionIDManagement" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_CONNECTIVITY=$subscriptionIDConnectivity" | Out-File -File + "SUBSCRIPTION_ID_IDENTITY=$subscriptionIDIdentity" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_SECURITY=$subscriptionIDSecurity" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + shell: pwsh + + - name: Install the Accelerator PowerShell Module + run: | + Write-Host "Installing the Accelerator PowerShell Module" + ${{ env.POWERSHELL_MODULE_FOLDER }}/actions_bootstrap_for_e2e_tests.ps1 | Out-String | Write-Verbose + Invoke-Build -File ${{ env.POWERSHELL_MODULE_FOLDER }}/src/ALZ.build.ps1 BuildAndInstallOnly | Out-String | Write-Verbose + Write-Host "Installed Accelerator Module" + + shell: pwsh + - name: Setup ALZ Module Inputs run: | @@ -242,7 +268,6 @@ jobs: } else { $Inputs["starter_locations"] = @($location) } - $Inputs["bootstrap_subscription_id"] = "" $Inputs["service_name"] = "alz" $Inputs["environment_name"] = $uniqueId $Inputs["postfix_number"] = "1" @@ -288,16 +313,14 @@ jobs: $rootParentManagementGroupId = $selfHostedAgents -eq "none" ? "${{ vars.NESTED_ROOT_PARENT_MANAGEMENT_GROUP_ID }}" : "" $Inputs["root_parent_management_group_id"] = $rootParentManagementGroupId - $subscriptionIDManagement = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-management" | ConvertFrom-Json | Select-Object -ExpandProperty id - $subscriptionIDConnectivity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-connectivity" | ConvertFrom-Json | Select-Object -ExpandProperty id - $subscriptionIDIdentity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-identity" | ConvertFrom-Json | Select-Object -ExpandProperty id - $subscriptionIDSecurity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-security" | ConvertFrom-Json | Select-Object -ExpandProperty id + # Subscription IDs + $Inputs["bootstrap_subscription_id"] = $env:SUBSCRIPTION_ID_BOOTSTRAP $Inputs["subscription_ids"] = @{ - management = $subscriptionIDManagement - connectivity = $subscriptionIDConnectivity - identity = $subscriptionIDIdentity - security = $subscriptionIDSecurity + management = $env:SUBSCRIPTION_ID_MANAGEMENT + connectivity = $env:SUBSCRIPTION_ID_CONNECTIVITY + identity = $env:SUBSCRIPTION_ID_IDENTITY + security = $env:SUBSCRIPTION_ID_SECURITY } # Test specific inputs @@ -332,8 +355,8 @@ jobs: # Bicep if($infrastructureAsCode -eq "bicep") { $Inputs["network_type"] = "none" - $Inputs["management_group_int_root_id"] = "alz-$uniqueId" - $Inputs["management_group_int_root_name"] = "alz-$uniqueId" + $Inputs["management_group_int_root_id"] = "test-$uniqueId" + $Inputs["management_group_int_root_name"] = "Test $uniqueId" $Inputs["management_group_id_prefix"] = "" $Inputs["management_group_id_postfix"] = "" $Inputs["management_group_name_prefix"] = "" @@ -348,6 +371,19 @@ jobs: shell: pwsh + - name: Clean Up Pre-Run + run: | + + $uniqueId = $env:UNIQUE_ID + + Remove-PlatformLandingZone ` + -ManagementGroups "test-$uniqueId" ` + -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` + -AdditionalSubscriptions $env:SUBSCRIPTION_ID_BOOTSTRAP ` + -DeleteTargetManagementGroups + + shell: pwsh + - name: Run ALZ PowerShell run: | @@ -355,12 +391,6 @@ jobs: $versionControlSystem = "${{ matrix.versionControlSystem }}" $infrastructureAsCode = "${{ matrix.infrastructureAsCode }}" - # Install the Module - Write-Host "Installing the Accelerator PowerShell Module" - ${{ env.POWERSHELL_MODULE_FOLDER }}/actions_bootstrap_for_e2e_tests.ps1 | Out-String | Write-Verbose - Invoke-Build -File ${{ env.POWERSHELL_MODULE_FOLDER }}/src/ALZ.build.ps1 BuildAndInstallOnly | Out-String | Write-Verbose - Write-Host "Installed Accelerator Module" - # Run the Module in a retry loop $retryCount = 0 $maximumRetries = 10 @@ -418,7 +448,6 @@ jobs: shell: pwsh env: ARM_TENANT_ID: ${{ vars.ARM_TENANT_ID }} - ARM_SUBSCRIPTION_ID: ${{ vars.ARM_SUBSCRIPTION_ID }} ARM_CLIENT_ID: ${{ vars.ARM_CLIENT_ID }} ARM_USE_OIDC: true @@ -487,21 +516,27 @@ jobs: - name: Run Terraform Destroy to Clean Up if: ${{ always() && ((inputs.skip_destroy != '' && inputs.skip_destroy || 'no') == 'no') }} run: | - # Get Inputs $versionControlSystem = "${{ matrix.versionControlSystem }}" - Write-Host "Installing the Accelerator PowerShell Module" - ${{ env.POWERSHELL_MODULE_FOLDER }}/actions_bootstrap_for_e2e_tests.ps1 | Out-String | Write-Verbose - Invoke-Build -File ${{ env.POWERSHELL_MODULE_FOLDER }}/src/ALZ.build.ps1 BuildAndInstallOnly | Out-String | Write-Verbose - Write-Host "Installed Accelerator Module" - # Run destroy ${{ env.BOOTSTRAP_MODULE_FOLDER }}/.github/tests/scripts/destroy.ps1 -versionControlSystem $versionControlSystem shell: pwsh env: ARM_TENANT_ID: ${{ vars.ARM_TENANT_ID }} - ARM_SUBSCRIPTION_ID: ${{ vars.ARM_SUBSCRIPTION_ID }} ARM_CLIENT_ID: ${{ vars.ARM_CLIENT_ID }} ARM_USE_OIDC: true + + - name: Clean Up Post-Run + run: | + $uniqueId = $env:UNIQUE_ID + + Remove-PlatformLandingZone ` + -ManagementGroups "test-$uniqueId" ` + -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` + -AdditionalSubscriptions $env:SUBSCRIPTION_ID_BOOTSTRAP ` + -DeleteTargetManagementGroups + + shell: pwsh + if: always() From 7d65ff393a2e47ed9803f1c027a06e5fbfe63e1e Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 10:58:49 +0000 Subject: [PATCH 28/42] fix missing variable --- .github/workflows/end-to-end-test.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index f4370cd1..27f54fc1 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -141,6 +141,15 @@ jobs: - name: Get Subscriptions run: | + $deployAzureResources = "${{ matrix.deployAzureResources }}" + if($deployAzureResources -eq "false") { + Write-Host "Skipping subscription retrieval as deployAzureResources is set to false." + "ARM_SUBSCRIPTION_ID=${{ vars.ARM_SUBSCRIPTION_ID }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + exit 0 + } + + $shortNamePrefix = "${{ matrix.ShortNamePrefix }}" + $subscriptionIDBootstrap = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-bootstrap" | ConvertFrom-Json | Select-Object -ExpandProperty id $subscriptionIDManagement = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-management" | ConvertFrom-Json | Select-Object -ExpandProperty id $subscriptionIDConnectivity = az account show --subscription "accelerator-bootstrap-modules-$shortNamePrefix-connectivity" | ConvertFrom-Json | Select-Object -ExpandProperty id @@ -373,6 +382,11 @@ jobs: - name: Clean Up Pre-Run run: | + $deployAzureResources = "${{ matrix.deployAzureResources }}" + if($deployAzureResources -eq "false") { + Write-Host "Skipping cleanup as deployAzureResources is set to false." + exit 0 + } $uniqueId = $env:UNIQUE_ID @@ -530,6 +544,12 @@ jobs: - name: Clean Up Post-Run run: | + $deployAzureResources = "${{ matrix.deployAzureResources }}" + if($deployAzureResources -eq "false") { + Write-Host "Skipping cleanup as deployAzureResources is set to false." + exit 0 + } + $uniqueId = $env:UNIQUE_ID Remove-PlatformLandingZone ` From 10690433e9af8d52d70dfec5367247a88615b8c2 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 11:11:15 +0000 Subject: [PATCH 29/42] fix copilot typo --- .github/workflows/end-to-end-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 27f54fc1..d26d958a 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -159,7 +159,7 @@ jobs: "ARM_SUBSCRIPTION_ID=$subscriptionIDBootstrap" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "SUBSCRIPTION_ID_BOOTSTRAP=$subscriptionIDBootstrap" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "SUBSCRIPTION_ID_MANAGEMENT=$subscriptionIDManagement" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "SUBSCRIPTION_ID_CONNECTIVITY=$subscriptionIDConnectivity" | Out-File -File + "SUBSCRIPTION_ID_CONNECTIVITY=$subscriptionIDConnectivity" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "SUBSCRIPTION_ID_IDENTITY=$subscriptionIDIdentity" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "SUBSCRIPTION_ID_SECURITY=$subscriptionIDSecurity" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append From 7f85c03b83a33257760f90df473ea35e6e3cff95 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 11:30:39 +0000 Subject: [PATCH 30/42] import module --- .github/workflows/end-to-end-test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index d26d958a..ffef7d86 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -390,6 +390,8 @@ jobs: $uniqueId = $env:UNIQUE_ID + Invoke-Build -File ${{ env.POWERSHELL_MODULE_FOLDER }}/src/ALZ.build.ps1 Install | Out-String | Write-Verbose + Remove-PlatformLandingZone ` -ManagementGroups "test-$uniqueId" ` -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` @@ -401,6 +403,8 @@ jobs: - name: Run ALZ PowerShell run: | + Invoke-Build -File ${{ env.POWERSHELL_MODULE_FOLDER }}/src/ALZ.build.ps1 Install | Out-String | Write-Verbose + # Get Inputs $versionControlSystem = "${{ matrix.versionControlSystem }}" $infrastructureAsCode = "${{ matrix.infrastructureAsCode }}" @@ -530,6 +534,8 @@ jobs: - name: Run Terraform Destroy to Clean Up if: ${{ always() && ((inputs.skip_destroy != '' && inputs.skip_destroy || 'no') == 'no') }} run: | + Invoke-Build -File ${{ env.POWERSHELL_MODULE_FOLDER }}/src/ALZ.build.ps1 Install | Out-String | Write-Verbose + # Get Inputs $versionControlSystem = "${{ matrix.versionControlSystem }}" @@ -552,6 +558,8 @@ jobs: $uniqueId = $env:UNIQUE_ID + Invoke-Build -File ${{ env.POWERSHELL_MODULE_FOLDER }}/src/ALZ.build.ps1 Install | Out-String | Write-Verbose + Remove-PlatformLandingZone ` -ManagementGroups "test-$uniqueId" ` -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` From 4fee80c79972d6554a40d0120a4fed33ee254fc2 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 11:42:07 +0000 Subject: [PATCH 31/42] sort cleanup script --- .github/workflows/end-to-end-test.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index ffef7d86..2d949399 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -396,7 +396,11 @@ jobs: -ManagementGroups "test-$uniqueId" ` -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` -AdditionalSubscriptions $env:SUBSCRIPTION_ID_BOOTSTRAP ` - -DeleteTargetManagementGroups + -DeleteTargetManagementGroups ` + -AllowNoManagementGroupMatch ` + -BypassConfirmation ` + -BypassConfirmationTimeoutSeconds 0 + shell: pwsh @@ -564,7 +568,10 @@ jobs: -ManagementGroups "test-$uniqueId" ` -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` -AdditionalSubscriptions $env:SUBSCRIPTION_ID_BOOTSTRAP ` - -DeleteTargetManagementGroups + -DeleteTargetManagementGroups ` + -AllowNoManagementGroupMatch ` + -BypassConfirmation ` + -BypassConfirmationTimeoutSeconds 0 shell: pwsh if: always() From 3763cf760837504092c233f8104f6ecfdb6aef5e Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 12:01:23 +0000 Subject: [PATCH 32/42] register resource providers --- .../tests/scripts/create-subscriptions.ps1 | 204 +++++++++++------- 1 file changed, 127 insertions(+), 77 deletions(-) diff --git a/.github/tests/scripts/create-subscriptions.ps1 b/.github/tests/scripts/create-subscriptions.ps1 index fdab2d12..21b9ef04 100644 --- a/.github/tests/scripts/create-subscriptions.ps1 +++ b/.github/tests/scripts/create-subscriptions.ps1 @@ -3,8 +3,10 @@ param( [string]$billingScope, [string]$subscriptionNamePrefix = "accelerator-bootstrap-modules", [string[]]$subscriptionTypes = @("connectivity", "management", "identity", "security", "bootstrap"), + [string[]]$resourceProviders = @("Microsoft.Security"), [int]$maxRetries = 5, [int]$throttleLimit = 2, + [int]$resourceProviderThrottleLimit = 10, [switch]$planOnly ) @@ -95,101 +97,149 @@ if ($existingSubscriptions.Count -gt 0) { Write-Host "" if ($subscriptionsToCreate.Count -eq 0) { Write-Host "No new subscriptions to create. All aliases already exist." -ForegroundColor Green - return } -Write-Host "=== Subscriptions to Create ===" -ForegroundColor Cyan -foreach ($sub in $subscriptionsToCreate) { - Write-Host " - $sub" -ForegroundColor Yellow +if ($subscriptionsToCreate.Count -gt 0) { + Write-Host "=== Subscriptions to Create ===" -ForegroundColor Cyan + foreach ($sub in $subscriptionsToCreate) { + Write-Host " - $sub" -ForegroundColor Yellow + } + Write-Host "" + Write-Host "Total: $($subscriptionsToCreate.Count) subscription(s) to create" -ForegroundColor Cyan + Write-Host "" } -Write-Host "" -Write-Host "Total: $($subscriptionsToCreate.Count) subscription(s) to create" -ForegroundColor Cyan -Write-Host "" if ($planOnly) { Write-Host "Plan only mode - no subscriptions will be created." -ForegroundColor Magenta return } -# Prompt for confirmation before creating -$createConfirmation = Read-Host "Do you want to create these $($subscriptionsToCreate.Count) subscription(s)? (y/n)" -if ($createConfirmation -ine 'y') { - Write-Host "Operation cancelled by user." -ForegroundColor Red - return -} +if ($subscriptionsToCreate.Count -gt 0) { + # Prompt for confirmation before creating + $createConfirmation = Read-Host "Do you want to create these $($subscriptionsToCreate.Count) subscription(s)? (y/n)" + if ($createConfirmation -ine 'y') { + Write-Host "Operation cancelled by user." -ForegroundColor Red + return + } -Write-Host "" + Write-Host "" -# Create a thread-safe hashtable to track rate limiting across parallel tasks -$rateLimitState = [hashtable]::Synchronized(@{ - WaitUntil = [DateTime]::MinValue -}) - -# Create the subscriptions in parallel with retry logic -Write-Host "Creating subscriptions (throttle: $throttleLimit)..." -ForegroundColor Cyan - -$results = $subscriptionsToCreate | ForEach-Object -Parallel { - $subscriptionName = $_ - $scope = $using:billingScope - $retries = $using:maxRetries - $state = $using:rateLimitState - $VerbosePreference = $using:VerbosePreference - $retryCount = 0 - $success = $false - - while (-not $success -and $retryCount -lt $retries) { - # Check if we're in a rate limit wait period - $waitUntil = $state.WaitUntil - if ($waitUntil -gt [DateTime]::Now) { - $waitSeconds = [math]::Ceiling(($waitUntil - [DateTime]::Now).TotalSeconds) - Write-Host "Rate limit active. $subscriptionName waiting $waitSeconds seconds..." -ForegroundColor Yellow - Start-Sleep -Seconds $waitSeconds - } + # Create a thread-safe hashtable to track rate limiting across parallel tasks + $rateLimitState = [hashtable]::Synchronized(@{ + WaitUntil = [DateTime]::MinValue + }) + + # Create the subscriptions in parallel with retry logic + Write-Host "Creating subscriptions (throttle: $throttleLimit)..." -ForegroundColor Cyan + + $results = $subscriptionsToCreate | ForEach-Object -Parallel { + $subscriptionName = $_ + $scope = $using:billingScope + $retries = $using:maxRetries + $state = $using:rateLimitState + $VerbosePreference = $using:VerbosePreference + $retryCount = 0 + $success = $false + + while (-not $success -and $retryCount -lt $retries) { + # Check if we're in a rate limit wait period + $waitUntil = $state.WaitUntil + if ($waitUntil -gt [DateTime]::Now) { + $waitSeconds = [math]::Ceiling(($waitUntil - [DateTime]::Now).TotalSeconds) + Write-Host "Rate limit active. $subscriptionName waiting $waitSeconds seconds..." -ForegroundColor Yellow + Start-Sleep -Seconds $waitSeconds + } - Write-Host "Creating subscription: $subscriptionName (Attempt $($retryCount + 1) of $retries)" -ForegroundColor Yellow - $result = az account alias create --name "$subscriptionName" --billing-scope "$scope" --display-name "$subscriptionName" --workload "Production" 2>&1 + Write-Host "Creating subscription: $subscriptionName (Attempt $($retryCount + 1) of $retries)" -ForegroundColor Yellow + $result = az account alias create --name "$subscriptionName" --billing-scope "$scope" --display-name "$subscriptionName" --workload "Production" 2>&1 - if ($LASTEXITCODE -eq 0) { - $success = $true - Write-Host "Successfully created: $subscriptionName" -ForegroundColor Green - } else { - $errorMessage = $result | Out-String - if ($errorMessage -match "TooManyRequests.*Retry in (\d{2}):(\d{2}):(\d{2})") { - $hours = [int]$Matches[1] - $minutes = [int]$Matches[2] - $seconds = [int]$Matches[3] - $waitSeconds = ($hours * 3600) + ($minutes * 60) + $seconds + (1 * 60) # Add 60 second buffer - Write-Verbose $errorMessage - - # Set the shared rate limit wait time - $newWaitUntil = [DateTime]::Now.AddSeconds($waitSeconds) - if ($newWaitUntil -gt $state.WaitUntil) { - $state.WaitUntil = $newWaitUntil - Write-Host "Rate limit hit! All tasks will wait until $($newWaitUntil.ToString('HH:mm:ss'))" -ForegroundColor Red - } - - Write-Host "Rate limited for $subscriptionName. Waiting $waitSeconds seconds before retry..." -ForegroundColor Yellow - Start-Sleep -Seconds $waitSeconds - $retryCount++ + if ($LASTEXITCODE -eq 0) { + $success = $true + Write-Host "Successfully created: $subscriptionName" -ForegroundColor Green } else { - Write-Host "Failed to create $subscriptionName : $errorMessage" -ForegroundColor Red - break + $errorMessage = $result | Out-String + if ($errorMessage -match "TooManyRequests.*Retry in (\d{2}):(\d{2}):(\d{2})") { + $hours = [int]$Matches[1] + $minutes = [int]$Matches[2] + $seconds = [int]$Matches[3] + $waitSeconds = ($hours * 3600) + ($minutes * 60) + $seconds + (1 * 60) # Add 60 second buffer + Write-Verbose $errorMessage + + # Set the shared rate limit wait time + $newWaitUntil = [DateTime]::Now.AddSeconds($waitSeconds) + if ($newWaitUntil -gt $state.WaitUntil) { + $state.WaitUntil = $newWaitUntil + Write-Host "Rate limit hit! All tasks will wait until $($newWaitUntil.ToString('HH:mm:ss'))" -ForegroundColor Red + } + + Write-Host "Rate limited for $subscriptionName. Waiting $waitSeconds seconds before retry..." -ForegroundColor Yellow + Start-Sleep -Seconds $waitSeconds + $retryCount++ + } else { + Write-Host "Failed to create $subscriptionName : $errorMessage" -ForegroundColor Red + break + } } } - } - [PSCustomObject]@{ - Name = $subscriptionName - Success = $success + [PSCustomObject]@{ + Name = $subscriptionName + Success = $success + } + } -ThrottleLimit $throttleLimit + + $successCount = ($results | Where-Object { $_.Success }).Count + $failCount = ($results | Where-Object { -not $_.Success }).Count + + Write-Host "" + Write-Host "Subscription creation complete." -ForegroundColor Green + Write-Host " Successful: $successCount" -ForegroundColor Green + if ($failCount -gt 0) { + Write-Host " Failed: $failCount" -ForegroundColor Red } -} -ThrottleLimit $throttleLimit +} -$successCount = ($results | Where-Object { $_.Success }).Count -$failCount = ($results | Where-Object { -not $_.Success }).Count +# Register resource providers for all subscriptions +if ($resourceProviders.Count -gt 0 -and -not $planOnly) { + Write-Host "" + Write-Host "=== Registering Resource Providers ===" -ForegroundColor Cyan + Write-Host "Providers: $($resourceProviders -join ', ')" -ForegroundColor Yellow + + $allSubscriptionNames = $subscriptionsToCreate + $existingSubscriptions + + # Get subscription IDs for all aliases and register providers + $allSubscriptionNames | ForEach-Object -Parallel { + $subscriptionName = $_ + $providers = $using:resourceProviders + $VerbosePreference = $using:VerbosePreference + + # Get the subscription ID from the alias + $aliasInfo = az account alias show --name "$subscriptionName" --output json 2>$null | ConvertFrom-Json + + if ($aliasInfo -and $aliasInfo.properties.subscriptionId) { + $subscriptionId = $aliasInfo.properties.subscriptionId + + foreach ($provider in $providers) { + # Check if provider is already registered + $providerState = az provider show --namespace $provider --subscription $subscriptionId --query "registrationState" --output tsv 2>$null + + if ($providerState -ine "Registered") { + Write-Host "Registering $provider for $subscriptionName ($subscriptionId)..." -ForegroundColor Yellow + az provider register --namespace $provider --subscription $subscriptionId --output none --wait + if ($LASTEXITCODE -eq 0) { + Write-Host " Registration initiated: $provider for $subscriptionName" -ForegroundColor Green + } else { + Write-Host " Failed to register: $provider for $subscriptionName" -ForegroundColor Red + } + } else { + Write-Host " Already registered: $provider for $subscriptionName" -ForegroundColor Gray + } + } + } else { + Write-Host " Could not get subscription ID for alias: $subscriptionName" -ForegroundColor Red + } + } -ThrottleLimit $resourceProviderThrottleLimit -Write-Host "" -Write-Host "Subscription creation complete." -ForegroundColor Green -Write-Host " Successful: $successCount" -ForegroundColor Green -if ($failCount -gt 0) { - Write-Host " Failed: $failCount" -ForegroundColor Red + Write-Host "" + Write-Host "Resource provider registration complete." -ForegroundColor Green } From 7f962e48723ca41499be35545721fa778450d166 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 12:05:22 +0000 Subject: [PATCH 33/42] fix message --- .github/tests/scripts/create-subscriptions.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/tests/scripts/create-subscriptions.ps1 b/.github/tests/scripts/create-subscriptions.ps1 index 21b9ef04..4f1929ef 100644 --- a/.github/tests/scripts/create-subscriptions.ps1 +++ b/.github/tests/scripts/create-subscriptions.ps1 @@ -227,16 +227,16 @@ if ($resourceProviders.Count -gt 0 -and -not $planOnly) { Write-Host "Registering $provider for $subscriptionName ($subscriptionId)..." -ForegroundColor Yellow az provider register --namespace $provider --subscription $subscriptionId --output none --wait if ($LASTEXITCODE -eq 0) { - Write-Host " Registration initiated: $provider for $subscriptionName" -ForegroundColor Green + Write-Host "Registration succeeded: $provider for $subscriptionName" -ForegroundColor Green } else { - Write-Host " Failed to register: $provider for $subscriptionName" -ForegroundColor Red + Write-Host "Failed to register: $provider for $subscriptionName" -ForegroundColor Red } } else { - Write-Host " Already registered: $provider for $subscriptionName" -ForegroundColor Gray + Write-Host "Already registered: $provider for $subscriptionName" -ForegroundColor Gray } } } else { - Write-Host " Could not get subscription ID for alias: $subscriptionName" -ForegroundColor Red + Write-Host "Could not get subscription ID for alias: $subscriptionName" -ForegroundColor Red } } -ThrottleLimit $resourceProviderThrottleLimit From 2212bfe8192d4633b60c4e5c0d10802052e9aba2 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 13:03:34 +0000 Subject: [PATCH 34/42] fix local tests --- .github/workflows/end-to-end-test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 2d949399..6abd046c 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -145,6 +145,11 @@ jobs: if($deployAzureResources -eq "false") { Write-Host "Skipping subscription retrieval as deployAzureResources is set to false." "ARM_SUBSCRIPTION_ID=${{ vars.ARM_SUBSCRIPTION_ID }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_BOOTSTRAP=${{ vars.ARM_SUBSCRIPTION_ID }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_MANAGEMENT=${{ vars.ARM_SUBSCRIPTION_ID }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_CONNECTIVITY=${{ vars.ARM_SUBSCRIPTION_ID }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_IDENTITY=${{ vars.ARM_SUBSCRIPTION_ID }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "SUBSCRIPTION_ID_SECURITY=${{ vars.ARM_SUBSCRIPTION_ID }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append exit 0 } From effc74d4d75ede9281019ada32f4c5da8156e559 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 15:30:47 +0000 Subject: [PATCH 35/42] add forced subscription placement --- .github/workflows/end-to-end-test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 6abd046c..01fe8df9 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -324,7 +324,8 @@ jobs: $Inputs["apply_approvers"] = @() # Target a nested parent MG for public to test that scenario - $rootParentManagementGroupId = $selfHostedAgents -eq "none" ? "${{ vars.NESTED_ROOT_PARENT_MANAGEMENT_GROUP_ID }}" : "" + $rootParentManagementGroupId = $selfHostedAgents -eq "none" ? "${{ vars.NESTED_ROOT_PARENT_MANAGEMENT_GROUP_ID }}" : "${{ vars.ARM_TENANT_ID}}" + "ROOT_PARENT_MANAGEMENT_GROUP_ID=$rootParentManagementGroupId" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append $Inputs["root_parent_management_group_id"] = $rootParentManagementGroupId # Subscription IDs @@ -401,6 +402,8 @@ jobs: -ManagementGroups "test-$uniqueId" ` -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` -AdditionalSubscriptions $env:SUBSCRIPTION_ID_BOOTSTRAP ` + -SubscriptionsTargetManagementGroup $env:ROOT_PARENT_MANAGEMENT_GROUP_ID ` + -ForceSubscriptionPlacement ` -DeleteTargetManagementGroups ` -AllowNoManagementGroupMatch ` -BypassConfirmation ` @@ -573,6 +576,8 @@ jobs: -ManagementGroups "test-$uniqueId" ` -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` -AdditionalSubscriptions $env:SUBSCRIPTION_ID_BOOTSTRAP ` + -SubscriptionsTargetManagementGroup $env:ROOT_PARENT_MANAGEMENT_GROUP_ID ` + -ForceSubscriptionPlacement ` -DeleteTargetManagementGroups ` -AllowNoManagementGroupMatch ` -BypassConfirmation ` From 5d04ed6d204c6be3ddb5ccaa37540f2a90638494 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 16:37:31 +0000 Subject: [PATCH 36/42] delete all management groups that match --- .github/workflows/end-to-end-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 01fe8df9..2a005ee7 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -395,11 +395,12 @@ jobs: } $uniqueId = $env:UNIQUE_ID + $shortNamePrefix = "${{ matrix.ShortNamePrefix }}" Invoke-Build -File ${{ env.POWERSHELL_MODULE_FOLDER }}/src/ALZ.build.ps1 Install | Out-String | Write-Verbose Remove-PlatformLandingZone ` - -ManagementGroups "test-$uniqueId" ` + -ManagementGroups "$shortNamePrefix" ` -Subscriptions $env:SUBSCRIPTION_ID_MANAGEMENT, $env:SUBSCRIPTION_ID_CONNECTIVITY, $env:SUBSCRIPTION_ID_IDENTITY, $env:SUBSCRIPTION_ID_SECURITY ` -AdditionalSubscriptions $env:SUBSCRIPTION_ID_BOOTSTRAP ` -SubscriptionsTargetManagementGroup $env:ROOT_PARENT_MANAGEMENT_GROUP_ID ` From c81362c50afc8729859fc4a0ba2da35afb620737 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 19:01:54 +0000 Subject: [PATCH 37/42] remove redundant extra role assignments --- alz/local/main.tf | 1 - alz/local/variables.tf | 11 ----------- modules/azure/role_assignments.tf | 24 ++---------------------- modules/azure/storage.tf | 7 ------- modules/azure/variables.tf | 12 ------------ 5 files changed, 2 insertions(+), 53 deletions(-) diff --git a/alz/local/main.tf b/alz/local/main.tf index ba7a9eac..f13ca3f2 100644 --- a/alz/local/main.tf +++ b/alz/local/main.tf @@ -34,7 +34,6 @@ module "azure" { use_private_networking = false custom_role_definitions = var.iac_type == "terraform" ? local.custom_role_definitions_terraform : (var.iac_type == "bicep" ? local.custom_role_definitions_bicep : local.custom_role_definitions_bicep_classic) role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : (var.iac_type == "bicep" ? var.role_assignments_bicep : var.role_assignments_bicep_classic) - additional_role_assignment_principal_ids = var.grant_permissions_to_current_user ? { current_user = data.azurerm_client_config.current.object_id } : {} storage_account_blob_soft_delete_enabled = var.storage_account_blob_soft_delete_enabled storage_account_blob_soft_delete_retention_days = var.storage_account_blob_soft_delete_retention_days storage_account_blob_versioning_enabled = var.storage_account_blob_versioning_enabled diff --git a/alz/local/variables.tf b/alz/local/variables.tf index f91125ca..6b0fbb70 100644 --- a/alz/local/variables.tf +++ b/alz/local/variables.tf @@ -188,17 +188,6 @@ variable "postfix_number" { default = 1 } -variable "grant_permissions_to_current_user" { - description = <<-EOT - **(Optional, default: `true`)** Whether to grant permissions to the current Azure CLI user. - - When true, assigns permissions to the currently authenticated user in addition to the managed identities. - Useful for local development and testing. - EOT - type = bool - default = true -} - variable "additional_files" { description = <<-EOT **(Optional, default: `[]`)** Additional files to include in the deployment. diff --git a/modules/azure/role_assignments.tf b/modules/azure/role_assignments.tf index d0bbc87a..8dfc7e33 100644 --- a/modules/azure/role_assignments.tf +++ b/modules/azure/role_assignments.tf @@ -7,28 +7,8 @@ locals { principal_id = azurerm_user_assigned_identity.alz[value.user_assigned_managed_identity_key].principal_id } } - additional_role_assignments = { for assignment in flatten([ - for key, value in var.role_assignments : [ - for princial_key, principal_value in var.additional_role_assignment_principal_ids : { - composite_key = "${value.scope}-${coalesce(value.custom_role_definition_key, value.built_in_role_definition_name)}-${princial_key}" - user_assigned_managed_identity_key = "${value.scope}-${coalesce(value.custom_role_definition_key, value.built_in_role_definition_name)}-${princial_key}" - built_in_role_definition_name = value.built_in_role_definition_name - custom_role_definition_key = value.custom_role_definition_key - scope = value.scope - principal_id = principal_value - } - ]]) : assignment.composite_key => { - user_assigned_managed_identity_key = assignment.user_assigned_managed_identity_key - built_in_role_definition_name = assignment.built_in_role_definition_name - custom_role_definition_key = assignment.custom_role_definition_key - scope = assignment.scope - principal_id = assignment.principal_id - } } - - combined_role_assignments = merge(local.role_assignments, local.additional_role_assignments) - subscription_role_assignments = { for assignment in flatten([ - for key, value in local.combined_role_assignments : [ + for key, value in local.role_assignments : [ for subscription_id, subscription in data.azurerm_subscription.alz : { key = "${value.user_assigned_managed_identity_key}-${coalesce(value.custom_role_definition_key, value.built_in_role_definition_name)}-${subscription_id}" scope = subscription.id @@ -45,7 +25,7 @@ locals { } } management_group_role_assignments = { - for key, value in local.combined_role_assignments : key => { + for key, value in local.role_assignments : key => { scope = var.intermediate_root_management_group_creation_enabled ? azapi_resource.intermediate_root_management_group[0].id : data.azurerm_management_group.alz.id role_definition_id = value.built_in_role_definition_name == null ? azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id : null role_definition_name = value.built_in_role_definition_name diff --git a/modules/azure/storage.tf b/modules/azure/storage.tf index ab33a4fd..1c62d7d3 100644 --- a/modules/azure/storage.tf +++ b/modules/azure/storage.tf @@ -64,10 +64,3 @@ resource "azurerm_role_assignment" "alz_storage_container" { role_definition_name = "Storage Blob Data Owner" principal_id = azurerm_user_assigned_identity.alz[each.key].principal_id } - -resource "azurerm_role_assignment" "alz_storage_container_additional" { - for_each = var.create_storage_account ? var.additional_role_assignment_principal_ids : {} - scope = azapi_resource.storage_account_container[0].id - role_definition_name = "Storage Blob Data Owner" - principal_id = each.value -} diff --git a/modules/azure/variables.tf b/modules/azure/variables.tf index c44c84da..94723fbb 100644 --- a/modules/azure/variables.tf +++ b/modules/azure/variables.tf @@ -610,18 +610,6 @@ variable "role_assignments" { })) } -variable "additional_role_assignment_principal_ids" { - description = <<-EOT - **(Optional, default: `{}`)** Additional Azure AD principal IDs to grant the same role assignments. - - Map of principal IDs (users, groups, service principals) to grant the same role assignments - as the managed identities. Useful for granting permissions to human operators or existing - service principals for troubleshooting or manual operations. - EOT - type = map(string) - default = {} -} - variable "tenant_role_assignment_enabled" { description = <<-EOT **(Optional, default: `false`)** Enable tenant-level role assignment for managed identities. From a04088a153ceaaed7e04ec5909b672d7fcaa6f80 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Wed, 28 Jan 2026 19:59:26 +0000 Subject: [PATCH 38/42] add another az login --- .github/workflows/end-to-end-test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 2a005ee7..1571214c 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -561,6 +561,13 @@ jobs: ARM_CLIENT_ID: ${{ vars.ARM_CLIENT_ID }} ARM_USE_OIDC: true + - name: Azure login for a new access token + uses: azure/login@v2 + with: + client-id: ${{ vars.ARM_CLIENT_ID }} + tenant-id: ${{ vars.ARM_TENANT_ID }} + subscription-id: ${{ vars.ARM_SUBSCRIPTION_ID }} + - name: Clean Up Post-Run run: | $deployAzureResources = "${{ matrix.deployAzureResources }}" From dedea9dba22f3550ecbcd4f08f67db78c40e012b Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 30 Jan 2026 13:53:14 +0000 Subject: [PATCH 39/42] remove redundant variable --- alz/azuredevops/main.tf | 1 - alz/azuredevops/variables.tf | 11 ----------- alz/github/main.tf | 1 - alz/github/variables.tf | 12 ------------ alz/local/main.tf | 1 - alz/local/variables.tf | 12 ------------ modules/file_manipulation/variables.tf | 10 ---------- 7 files changed, 48 deletions(-) diff --git a/alz/azuredevops/main.tf b/alz/azuredevops/main.tf index da68bd37..2ccedb80 100644 --- a/alz/azuredevops/main.tf +++ b/alz/azuredevops/main.tf @@ -125,5 +125,4 @@ module "file_manipulation" { pipeline_files_directory_path = local.pipeline_files_directory_path pipeline_template_files_directory_path = local.pipeline_template_files_directory_path terraform_architecture_file_path = var.terraform_architecture_file_path - terraform_intermediate_root_management_group_state_resource_path_for_import = var.terraform_intermediate_root_management_group_state_resource_path_for_import } \ No newline at end of file diff --git a/alz/azuredevops/variables.tf b/alz/azuredevops/variables.tf index 3767c53c..c7731217 100644 --- a/alz/azuredevops/variables.tf +++ b/alz/azuredevops/variables.tf @@ -989,14 +989,3 @@ variable "terraform_architecture_file_path" { type = string default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" } - -variable "terraform_intermediate_root_management_group_state_resource_path_for_import" { - description = <<-EOT - **(Optional, default: `null`)** Resource path for the management group in the Terraform architecture. - - Used for generating accurate resource references in Terraform deployments. - Null when not applicable. - EOT - type = string - default = "module.management_groups[0].module.management_groups.azapi_resource.management_groups_level_0" -} diff --git a/alz/github/main.tf b/alz/github/main.tf index bcda742b..e283188a 100644 --- a/alz/github/main.tf +++ b/alz/github/main.tf @@ -127,5 +127,4 @@ module "file_manipulation" { pipeline_template_files_directory_path = local.pipeline_template_files_directory_path concurrency_value = local.resource_names.storage_container terraform_architecture_file_path = var.terraform_architecture_file_path - terraform_intermediate_root_management_group_state_resource_path_for_import = var.terraform_intermediate_root_management_group_state_resource_path_for_import } diff --git a/alz/github/variables.tf b/alz/github/variables.tf index 484ceaa9..ef90d395 100644 --- a/alz/github/variables.tf +++ b/alz/github/variables.tf @@ -1041,15 +1041,3 @@ variable "terraform_architecture_file_path" { type = string default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" } - -variable "terraform_intermediate_root_management_group_state_resource_path_for_import" { - description = <<-EOT - **(Optional, default: `null`)** Resource path for the management group in the Terraform architecture. - - Used for generating accurate resource references in Terraform deployments. - Null when not applicable. - EOT - type = string - default = "module.management_groups[0].module.management_groups.azapi_resource.management_groups_level_0" -} - diff --git a/alz/local/main.tf b/alz/local/main.tf index f13ca3f2..8bedb631 100644 --- a/alz/local/main.tf +++ b/alz/local/main.tf @@ -63,7 +63,6 @@ module "file_manipulation" { bicep_parameters_file_path = var.bicep_parameters_file_path pipeline_files_directory_path = local.script_source_folder_path terraform_architecture_file_path = var.terraform_architecture_file_path - terraform_intermediate_root_management_group_state_resource_path_for_import = var.terraform_intermediate_root_management_group_state_resource_path_for_import } resource "local_file" "alz" { diff --git a/alz/local/variables.tf b/alz/local/variables.tf index 6b0fbb70..fd1152c5 100644 --- a/alz/local/variables.tf +++ b/alz/local/variables.tf @@ -729,15 +729,3 @@ variable "terraform_architecture_file_path" { type = string default = "lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" } - -variable "terraform_intermediate_root_management_group_state_resource_path_for_import" { - description = <<-EOT - **(Optional, default: `null`)** Resource path for the management group in the Terraform architecture. - - Used for generating accurate resource references in Terraform deployments. - Null when not applicable. - EOT - type = string - default = "module.management_groups[0].module.management_groups.azapi_resource.management_groups_level_0" -} - diff --git a/modules/file_manipulation/variables.tf b/modules/file_manipulation/variables.tf index ec011c4b..f0971fbc 100644 --- a/modules/file_manipulation/variables.tf +++ b/modules/file_manipulation/variables.tf @@ -240,13 +240,3 @@ variable "terraform_architecture_file_path" { EOT type = string } - -variable "terraform_intermediate_root_management_group_state_resource_path_for_import" { - description = <<-EOT - **(Optional, default: `null`)** Resource path for the management group in the Terraform architecture. - - Used for generating accurate resource references in Terraform deployments. - Null when not applicable. - EOT - type = string -} From 4dda7c3e6e4bc5f0da4faf61e626b102286f0fa3 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 30 Jan 2026 15:09:31 +0000 Subject: [PATCH 40/42] linting --- alz/azuredevops/main.tf | 44 +++++++++++++++++++-------------------- alz/github/main.tf | 46 ++++++++++++++++++++--------------------- alz/local/main.tf | 30 +++++++++++++-------------- 3 files changed, 60 insertions(+), 60 deletions(-) diff --git a/alz/azuredevops/main.tf b/alz/azuredevops/main.tf index 2ccedb80..48819723 100644 --- a/alz/azuredevops/main.tf +++ b/alz/azuredevops/main.tf @@ -103,26 +103,26 @@ module "azure_devops" { } module "file_manipulation" { - source = "../../modules/file_manipulation" - vcs_type = "azuredevops" - files = module.files.files - use_self_hosted_agents_runners = var.use_self_hosted_agents - resource_names = local.resource_names - use_separate_repository_for_templates = var.use_separate_repository_for_templates - iac_type = var.iac_type - module_folder_path = local.starter_module_folder_path - bicep_config_file_path = var.bicep_config_file_path - starter_module_name = var.starter_module_name - project_or_organization_name = var.azure_devops_project_name - root_module_folder_relative_path = var.root_module_folder_relative_path - on_demand_folder_repository = var.on_demand_folder_repository - on_demand_folder_artifact_name = var.on_demand_folder_artifact_name - ci_template_file_name = local.ci_template_file_name - cd_template_file_name = local.cd_template_file_name - pipeline_target_folder_name = local.target_folder_name - bicep_parameters_file_path = var.bicep_parameters_file_path - agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration - pipeline_files_directory_path = local.pipeline_files_directory_path - pipeline_template_files_directory_path = local.pipeline_template_files_directory_path - terraform_architecture_file_path = var.terraform_architecture_file_path + source = "../../modules/file_manipulation" + vcs_type = "azuredevops" + files = module.files.files + use_self_hosted_agents_runners = var.use_self_hosted_agents + resource_names = local.resource_names + use_separate_repository_for_templates = var.use_separate_repository_for_templates + iac_type = var.iac_type + module_folder_path = local.starter_module_folder_path + bicep_config_file_path = var.bicep_config_file_path + starter_module_name = var.starter_module_name + project_or_organization_name = var.azure_devops_project_name + root_module_folder_relative_path = var.root_module_folder_relative_path + on_demand_folder_repository = var.on_demand_folder_repository + on_demand_folder_artifact_name = var.on_demand_folder_artifact_name + ci_template_file_name = local.ci_template_file_name + cd_template_file_name = local.cd_template_file_name + pipeline_target_folder_name = local.target_folder_name + bicep_parameters_file_path = var.bicep_parameters_file_path + agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration + pipeline_files_directory_path = local.pipeline_files_directory_path + pipeline_template_files_directory_path = local.pipeline_template_files_directory_path + terraform_architecture_file_path = var.terraform_architecture_file_path } \ No newline at end of file diff --git a/alz/github/main.tf b/alz/github/main.tf index e283188a..2249a74a 100644 --- a/alz/github/main.tf +++ b/alz/github/main.tf @@ -104,27 +104,27 @@ module "github" { } module "file_manipulation" { - source = "../../modules/file_manipulation" - vcs_type = "github" - files = module.files.files - use_self_hosted_agents_runners = var.use_self_hosted_runners - resource_names = local.resource_names - use_separate_repository_for_templates = var.use_separate_repository_for_templates - iac_type = var.iac_type - module_folder_path = local.starter_module_folder_path - bicep_config_file_path = var.bicep_config_file_path - starter_module_name = var.starter_module_name - project_or_organization_name = var.github_organization_name - root_module_folder_relative_path = var.root_module_folder_relative_path - on_demand_folder_repository = var.on_demand_folder_repository - on_demand_folder_artifact_name = var.on_demand_folder_artifact_name - ci_template_file_name = local.ci_template_file_name - cd_template_file_name = local.cd_template_file_name - pipeline_target_folder_name = local.target_folder_name - bicep_parameters_file_path = var.bicep_parameters_file_path - agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration - pipeline_files_directory_path = local.pipeline_files_directory_path - pipeline_template_files_directory_path = local.pipeline_template_files_directory_path - concurrency_value = local.resource_names.storage_container - terraform_architecture_file_path = var.terraform_architecture_file_path + source = "../../modules/file_manipulation" + vcs_type = "github" + files = module.files.files + use_self_hosted_agents_runners = var.use_self_hosted_runners + resource_names = local.resource_names + use_separate_repository_for_templates = var.use_separate_repository_for_templates + iac_type = var.iac_type + module_folder_path = local.starter_module_folder_path + bicep_config_file_path = var.bicep_config_file_path + starter_module_name = var.starter_module_name + project_or_organization_name = var.github_organization_name + root_module_folder_relative_path = var.root_module_folder_relative_path + on_demand_folder_repository = var.on_demand_folder_repository + on_demand_folder_artifact_name = var.on_demand_folder_artifact_name + ci_template_file_name = local.ci_template_file_name + cd_template_file_name = local.cd_template_file_name + pipeline_target_folder_name = local.target_folder_name + bicep_parameters_file_path = var.bicep_parameters_file_path + agent_pool_or_runner_configuration = local.agent_pool_or_runner_configuration + pipeline_files_directory_path = local.pipeline_files_directory_path + pipeline_template_files_directory_path = local.pipeline_template_files_directory_path + concurrency_value = local.resource_names.storage_container + terraform_architecture_file_path = var.terraform_architecture_file_path } diff --git a/alz/local/main.tf b/alz/local/main.tf index 8bedb631..758ccac2 100644 --- a/alz/local/main.tf +++ b/alz/local/main.tf @@ -48,21 +48,21 @@ module "azure" { } module "file_manipulation" { - source = "../../modules/file_manipulation" - vcs_type = "local" - files = module.files.files - resource_names = local.resource_names - iac_type = var.iac_type - module_folder_path = local.starter_module_folder_path - bicep_config_file_path = var.bicep_config_file_path - starter_module_name = var.starter_module_name - root_module_folder_relative_path = var.root_module_folder_relative_path - on_demand_folder_repository = var.on_demand_folder_repository - on_demand_folder_artifact_name = var.on_demand_folder_artifact_name - pipeline_target_folder_name = local.script_target_folder_name - bicep_parameters_file_path = var.bicep_parameters_file_path - pipeline_files_directory_path = local.script_source_folder_path - terraform_architecture_file_path = var.terraform_architecture_file_path + source = "../../modules/file_manipulation" + vcs_type = "local" + files = module.files.files + resource_names = local.resource_names + iac_type = var.iac_type + module_folder_path = local.starter_module_folder_path + bicep_config_file_path = var.bicep_config_file_path + starter_module_name = var.starter_module_name + root_module_folder_relative_path = var.root_module_folder_relative_path + on_demand_folder_repository = var.on_demand_folder_repository + on_demand_folder_artifact_name = var.on_demand_folder_artifact_name + pipeline_target_folder_name = local.script_target_folder_name + bicep_parameters_file_path = var.bicep_parameters_file_path + pipeline_files_directory_path = local.script_source_folder_path + terraform_architecture_file_path = var.terraform_architecture_file_path } resource "local_file" "alz" { From f453c42582f6cef5f4ba28129d5d47e484eaa188 Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 30 Jan 2026 15:37:01 +0000 Subject: [PATCH 41/42] fix storage container perms for local --- alz/local/main.tf | 1 + modules/azure/data.tf | 2 ++ modules/azure/storage.tf | 9 ++++++++- modules/azure/variables.tf | 12 ++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/alz/local/main.tf b/alz/local/main.tf index 758ccac2..79f7ec2a 100644 --- a/alz/local/main.tf +++ b/alz/local/main.tf @@ -34,6 +34,7 @@ module "azure" { use_private_networking = false custom_role_definitions = var.iac_type == "terraform" ? local.custom_role_definitions_terraform : (var.iac_type == "bicep" ? local.custom_role_definitions_bicep : local.custom_role_definitions_bicep_classic) role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : (var.iac_type == "bicep" ? var.role_assignments_bicep : var.role_assignments_bicep_classic) + additional_role_assignment_principal_ids = { current_user = data.azurerm_client_config.current.object_id } storage_account_blob_soft_delete_enabled = var.storage_account_blob_soft_delete_enabled storage_account_blob_soft_delete_retention_days = var.storage_account_blob_soft_delete_retention_days storage_account_blob_versioning_enabled = var.storage_account_blob_versioning_enabled diff --git a/modules/azure/data.tf b/modules/azure/data.tf index d9983576..a8c0f356 100644 --- a/modules/azure/data.tf +++ b/modules/azure/data.tf @@ -1,3 +1,5 @@ +data "azurerm_client_config" "alz" {} + data "azurerm_subscription" "alz" { for_each = local.subscription_ids subscription_id = each.key diff --git a/modules/azure/storage.tf b/modules/azure/storage.tf index 1c62d7d3..cd70d7f8 100644 --- a/modules/azure/storage.tf +++ b/modules/azure/storage.tf @@ -61,6 +61,13 @@ resource "azapi_resource" "storage_account_container" { resource "azurerm_role_assignment" "alz_storage_container" { for_each = var.create_storage_account ? var.user_assigned_managed_identities : {} scope = azapi_resource.storage_account_container[0].id - role_definition_name = "Storage Blob Data Owner" + role_definition_name = "Storage Blob Data Contributor" principal_id = azurerm_user_assigned_identity.alz[each.key].principal_id } + +resource "azurerm_role_assignment" "alz_storage_container_additional" { + for_each = var.create_storage_account ? var.additional_role_assignment_principal_ids : {} + scope = azapi_resource.storage_account_container[0].id + role_definition_name = "Storage Blob Data Contributor" + principal_id = each.value +} diff --git a/modules/azure/variables.tf b/modules/azure/variables.tf index 94723fbb..c44c84da 100644 --- a/modules/azure/variables.tf +++ b/modules/azure/variables.tf @@ -610,6 +610,18 @@ variable "role_assignments" { })) } +variable "additional_role_assignment_principal_ids" { + description = <<-EOT + **(Optional, default: `{}`)** Additional Azure AD principal IDs to grant the same role assignments. + + Map of principal IDs (users, groups, service principals) to grant the same role assignments + as the managed identities. Useful for granting permissions to human operators or existing + service principals for troubleshooting or manual operations. + EOT + type = map(string) + default = {} +} + variable "tenant_role_assignment_enabled" { description = <<-EOT **(Optional, default: `false`)** Enable tenant-level role assignment for managed identities. From 7d5efed90b11af5edcf2e5cdc49d3cefc592c0bb Mon Sep 17 00:00:00 2001 From: Jared Holgate Date: Fri, 30 Jan 2026 15:46:59 +0000 Subject: [PATCH 42/42] bin off uksouth --- .github/workflows/end-to-end-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 1571214c..9b21ca05 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -195,7 +195,7 @@ jobs: $deployAzureResources = "${{ matrix.deployAzureResources }}" $locations_with_aci_zone_support = @( - "uksouth", + # "uksouth", "northeurope", "eastus2", "westeurope",