├── .artifactignore ├── templates ├── platform_landing_zone │ ├── modules │ │ ├── management_groups │ │ │ ├── terraform.tf │ │ │ ├── locals.tf │ │ │ ├── main.tf │ │ │ └── variables.tf │ │ ├── management_resources │ │ │ ├── outputs.tf │ │ │ ├── terraform.tf │ │ │ ├── main.tf │ │ │ └── variables.tf │ │ └── config-templating │ │ │ ├── outputs.tf │ │ │ ├── terraform.tf │ │ │ ├── data.tf │ │ │ ├── variables.tf │ │ │ └── locals.config.tf │ ├── lib │ │ ├── archetype_definitions │ │ │ ├── root_custom.alz_archetype_override.yaml │ │ │ ├── online_custom.alz_archetype_override.yaml │ │ │ ├── sandbox_custom.alz_archetype_override.yaml │ │ │ ├── identity_custom.alz_archetype_override.yaml │ │ │ ├── security_custom.alz_archetype_override.yaml │ │ │ ├── management_custom.alz_archetype_override.yaml │ │ │ ├── decommissioned_custom.alz_archetype_override.yaml │ │ │ ├── corp_custom.alz_archetype_override.yaml │ │ │ ├── connectivity_custom.alz_archetype_override.yaml │ │ │ ├── platform_custom.alz_archetype_override.yaml │ │ │ └── landing_zones_custom.alz_archetype_override.yaml │ │ ├── alz_library_metadata.json │ │ └── architecture_definitions │ │ │ └── alz_custom.alz_architecture_definition.yaml │ ├── examples │ │ ├── slz │ │ │ └── lib │ │ │ │ ├── archetype_definitions │ │ │ │ ├── public_custom.alz_archetype_override.yaml │ │ │ │ ├── sovereign_root.alz_archetype_override.yaml │ │ │ │ ├── confidential_online_custom.alz_archetype_override.yaml │ │ │ │ └── confidential_corp_custom.alz_archetype_override.yaml │ │ │ │ ├── alz_library_metadata.json │ │ │ │ └── architecture_definitions │ │ │ │ └── alz_custom.alz_architecture_definition.yaml │ │ ├── management-only │ │ │ ├── README.md │ │ │ └── management.tfvars │ │ ├── migration │ │ │ └── lib │ │ │ │ ├── archetype_definitions │ │ │ │ └── root_custom.alz_archetype_override.yaml │ │ │ │ ├── role_definitions │ │ │ │ ├── network_management.alz_role_definition.json │ │ │ │ ├── application_owners.alz_role_definition.json │ │ │ │ ├── subscription_owner.alz_role_definition.json │ │ │ │ ├── network_subnet_contributor.alz_role_definition.json │ │ │ │ └── security_operations.alz_role_definition.json │ │ │ │ └── architecture_definitions │ │ │ │ └── alz_custom.alz_architecture_definition.yaml │ │ ├── full-multi-region │ │ │ └── README.md │ │ ├── full-single-region │ │ │ └── README.md │ │ ├── full-multi-region-nva │ │ │ └── README.md │ │ ├── full-single-region-nva │ │ │ └── README.md │ │ └── bootstrap │ │ │ ├── inputs-github.yaml │ │ │ ├── inputs-local.yaml │ │ │ └── inputs-azure-devops.yaml │ ├── variables.moved.tf │ ├── main.connectivity.virtual.wan.tf │ ├── main.resource.groups.tf │ ├── main.connectivity.hub.and.spoke.virtual.network.tf │ ├── outputs.moved.tf │ ├── main.management.tf │ ├── variables.connectivity.tf │ ├── main.config.tf │ ├── .gitignore │ ├── README.md │ ├── terraform.tf │ ├── locals.tf │ ├── variables.tf │ └── outputs.tf ├── test │ ├── README.md │ ├── outputs.tf │ ├── terraform.tf │ ├── variables.tf │ └── main.tf ├── test_nested │ ├── modules │ │ └── test_module │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ ├── terraform.tf │ │ │ └── variables.tf │ ├── test_root │ │ ├── README.md │ │ ├── outputs.tf │ │ ├── terraform.tf │ │ ├── variables.tf │ │ └── main.tf │ └── test_to_remove │ │ ├── README.md │ │ ├── outputs.tf │ │ ├── terraform.tf │ │ ├── variables.tf │ │ └── main.tf ├── empty │ ├── variables.tf │ ├── .gitignore │ ├── terraform.tf │ ├── README.md │ └── BOOTSTRAP.md └── .config │ └── ALZ-Powershell.config.json ├── .devcontainer └── devcontainer.json ├── docs └── wiki │ └── Home.md ├── .github ├── linters │ ├── .yaml-lint.yml │ └── .markdown-lint.yml ├── ISSUE_TEMPLATE │ └── config.yml ├── dependabot.yml ├── tests │ ├── scripts │ │ ├── examine-plan-file.ps1 │ │ ├── test-cleanup.ps1 │ │ ├── generate-matrix.ps1 │ │ └── terraform-run.ps1 │ └── subscription_lookup │ │ └── main.tf ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── fmt-test.yml │ ├── super-linter.yml │ ├── release.yml │ ├── wiki-sync.yml │ ├── scorecard.yml │ ├── pr-test.yml │ └── end-to-end-test.yml ├── CODE_OF_CONDUCT.md ├── SUPPORT.md ├── Makefile ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md └── DEVELOPER.md /.artifactignore: -------------------------------------------------------------------------------- 1 | **/.git/** 2 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/management_groups/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.12" 3 | } 4 | -------------------------------------------------------------------------------- /templates/test/README.md: -------------------------------------------------------------------------------- 1 | # Test Module 2 | 3 | This module is used for testing the bootstrapping without needing a clean tenant. 4 | -------------------------------------------------------------------------------- /templates/test_nested/modules/test_module/main.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_subscription" "current" { 2 | subscription_id = var.subscription_id 3 | } 4 | -------------------------------------------------------------------------------- /templates/test_nested/modules/test_module/outputs.tf: -------------------------------------------------------------------------------- 1 | output "subscription_name" { 2 | value = data.azurerm_subscription.current.display_name 3 | } 4 | -------------------------------------------------------------------------------- /templates/test_nested/test_root/README.md: -------------------------------------------------------------------------------- 1 | # Test Module 2 | 3 | This module is used for testing the bootstrapping without needing a clean tenant. 4 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/management_resources/outputs.tf: -------------------------------------------------------------------------------- 1 | output "management_resources" { 2 | value = module.management_resources 3 | } 4 | -------------------------------------------------------------------------------- /templates/test_nested/test_to_remove/README.md: -------------------------------------------------------------------------------- 1 | # Test Module 2 | 3 | This module is used for testing the bootstrapping without needing a clean tenant. 4 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/config-templating/outputs.tf: -------------------------------------------------------------------------------- 1 | output "custom_replacements" { 2 | value = local.final_replacements 3 | } 4 | 5 | output "outputs" { 6 | value = local.outputs 7 | } 8 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers/features/terraform:1": {}, 5 | "ghcr.io/devcontainers/features/azure-cli:1": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/wiki/Home.md: -------------------------------------------------------------------------------- 1 | 2 | Welcome to the Azure landing zones accelerators! 3 | 4 | Our documentation has moved over here: [aka.ms/alz/accelerator/docs](https://aka.ms/alz/accelerator/docs) 5 | -------------------------------------------------------------------------------- /templates/test_nested/modules/test_module/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.12" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 4.0" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /templates/test_nested/modules/test_module/variables.tf: -------------------------------------------------------------------------------- 1 | variable "subscription_id" { 2 | description = "The identifier of the Connectivity Subscription. (e.g '00000000-0000-0000-0000-000000000000')|azure_subscription_id" 3 | type = string 4 | } 5 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/config-templating/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.12" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 4.0" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/management_resources/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.12" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 4.0" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/linters/.yaml-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | # 200 chars should be enough, but don't fail if a line is longer 6 | line-length: 7 | max: 200 8 | level: warning 9 | truthy: 10 | check-keys: false 11 | level: warning 12 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/config-templating/data.tf: -------------------------------------------------------------------------------- 1 | module "regions" { 2 | source = "Azure/avm-utl-regions/azurerm" 3 | version = "0.9.2" 4 | use_cached_data = false 5 | enable_telemetry = var.enable_telemetry 6 | } 7 | 8 | data "azurerm_client_config" "current" {} 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: Azure Landing Zones Accelerator Issues 5 | url: https://github.com/Azure/ALZ-PowerShell-Module/issues/new/choose 6 | about: Please raise all issues for the Accelerators over at the Azure/ALZ-PowerShell-Module repository. 7 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/root_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: root 2 | name: root_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: [] 10 | role_definitions_to_remove: [] 11 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/online_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: online 2 | name: online_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: [] 10 | role_definitions_to_remove: [] 11 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/sandbox_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: sandbox 2 | name: sandbox_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: [] 10 | role_definitions_to_remove: [] 11 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/identity_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: identity 2 | name: identity_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: [] 10 | role_definitions_to_remove: [] 11 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/security_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: security 2 | name: security_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: [] 10 | role_definitions_to_remove: [] 11 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/slz/lib/archetype_definitions/public_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: public 2 | name: public_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: [] 10 | role_definitions_to_remove: [] 11 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/management_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: management 2 | name: management_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: [] 10 | role_definitions_to_remove: [] 11 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/decommissioned_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: decommissioned 2 | name: decommissioned_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: [] 10 | role_definitions_to_remove: [] 11 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/variables.moved.tf: -------------------------------------------------------------------------------- 1 | variable "private_link_private_dns_zone_virtual_network_link_moved_blocks_enabled" { 2 | description = < value if try(value.settings.enabled, true) } 6 | 7 | name = each.value.name 8 | location = each.value.location 9 | enable_telemetry = var.enable_telemetry 10 | tags = try(each.value.tags, null) == null ? module.config.outputs.tags : each.value.tags 11 | 12 | providers = { 13 | azurerm = azurerm.connectivity 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/main.connectivity.hub.and.spoke.virtual.network.tf: -------------------------------------------------------------------------------- 1 | module "hub_and_spoke_vnet" { 2 | source = "Azure/avm-ptn-alz-connectivity-hub-and-spoke-vnet/azurerm" 3 | version = "0.16.3" 4 | 5 | count = local.connectivity_hub_and_spoke_vnet_enabled ? 1 : 0 6 | 7 | hub_and_spoke_networks_settings = local.hub_and_spoke_networks_settings 8 | hub_virtual_networks = local.hub_virtual_networks 9 | enable_telemetry = var.enable_telemetry 10 | tags = coalesce(module.config.outputs.connectivity_tags, module.config.outputs.tags) 11 | 12 | providers = { 13 | azurerm = azurerm.connectivity 14 | azapi = azapi.connectivity 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/management-only/README.md: -------------------------------------------------------------------------------- 1 | # Platform landing zone - Managment Only 2 | 3 | This example configuration deploys the management groups and management resources only 4 | 5 | - Management Groups 6 | - Policy 7 | - Management Resources 8 | 9 | ## Options 10 | 11 | There is one option for deploying: 12 | 13 | - Hub and Spoke VNet: [hub-and-spoke-vnet.tfvars](./hub-and-spoke-vnet.tfvars) 14 | - Virtual WAN: [virtual-wan.tfvars](./virtual-wan.tfvars) 15 | 16 | ## Documentation 17 | 18 | The full documentation for this example can be found over at out [Azure Landing Zones documentation site](https://azure.github.io/Azure-Landing-Zones/accelerator/startermodules/terraform-platform-landing-zone/scenarios/). 19 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/platform_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: platform 2 | name: platform_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [ 5 | # To disable AMA policies, uncomment the following lines: 6 | # DenyAction-DeleteUAMIAMA, 7 | # Deploy-MDFC-DefSQL-AMA, 8 | # Deploy-VM-ChangeTrack, 9 | # Deploy-VM-Monitoring, 10 | # Deploy-vmArc-ChangeTrack, 11 | # Deploy-vmHybr-Monitoring, 12 | # Deploy-VMSS-ChangeTrack, 13 | # Deploy-VMSS-Monitoring, 14 | ] 15 | policy_definitions_to_add: [] 16 | policy_definitions_to_remove: [] 17 | policy_set_definitions_to_add: [] 18 | policy_set_definitions_to_remove: [] 19 | role_definitions_to_add: [] 20 | role_definitions_to_remove: [] 21 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/migration/lib/archetype_definitions/root_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | name: root_custom 2 | base_archetype: root 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [] 5 | policy_definitions_to_add: [] 6 | policy_definitions_to_remove: [] 7 | policy_set_definitions_to_add: [] 8 | policy_set_definitions_to_remove: [] 9 | role_definitions_to_add: 10 | - "[ALZ] Application-Owners" 11 | - "[ALZ] Network-Subnet-Contributor" 12 | - "[ALZ] Network-Management" 13 | - "[ALZ] Security-Operations" 14 | - "[ALZ] Subscription-Owner" 15 | role_definitions_to_remove: 16 | - "Application-Owners" 17 | - "Network-Subnet-Contributor" 18 | - "Network-Management" 19 | - "Security-Operations" 20 | - "Subscription-Owner" 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | @echo "==> Type make to run tasks" 3 | @echo 4 | @echo "Thing is one of:" 5 | @echo "fmt fmtcheck tfclean" 6 | 7 | fmt: 8 | @echo "==> Fixing Terraform code with terraform fmt..." 9 | terraform fmt -recursive 10 | 11 | fmtcheck: 12 | @echo "==> Checking source code with terraform fmt..." 13 | terraform fmt -check -recursive 14 | 15 | tfclean: 16 | @echo "==> Cleaning terraform files..." 17 | find . -type d -name '.terraform' | xargs rm -vrf 18 | find . -type f -name 'tfplan' | xargs rm -vf 19 | find . -type f -name 'terraform.tfstate*' | xargs rm -vf 20 | find . -type f -name '.terraform.lock.hcl' | xargs rm -vf 21 | 22 | # Makefile targets are files, but we aren't using it like this, 23 | # so have to declare PHONY targets 24 | .PHONY: fmt fmtcheck tfclean 25 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/lib/archetype_definitions/landing_zones_custom.alz_archetype_override.yaml: -------------------------------------------------------------------------------- 1 | base_archetype: landing_zones 2 | name: landing_zones_custom 3 | policy_assignments_to_add: [] 4 | policy_assignments_to_remove: [ 5 | # To remove AMA policies, uncomment the following lines: 6 | # Deploy-MDFC-DefSQL-AMA, 7 | # Deploy-VM-ChangeTrack, 8 | # Deploy-VM-Monitoring, 9 | # Deploy-vmArc-ChangeTrack, 10 | # Deploy-vmHybr-Monitoring, 11 | # Deploy-VMSS-ChangeTrack, 12 | # Deploy-VMSS-Monitoring, 13 | # To remove the DDOS modify policy, uncomment the following line: 14 | # Enable-DDoS-VNET, 15 | ] 16 | policy_definitions_to_add: [] 17 | policy_definitions_to_remove: [] 18 | policy_set_definitions_to_add: [] 19 | policy_set_definitions_to_remove: [] 20 | role_definitions_to_add: [] 21 | role_definitions_to_remove: [] 22 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/outputs.moved.tf: -------------------------------------------------------------------------------- 1 | output "private_link_private_dns_zone_virtual_network_link_moved_block_template_module_prefix" { 2 | description = < 22 | 23 | ## Documentation 24 | 25 | The full documentation for this example can be found over at out [Azure Landing Zones documentation site](https://azure.github.io/Azure-Landing-Zones/accelerator/startermodules/terraform-platform-landing-zone/scenarios/). 26 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/full-single-region-nva/README.md: -------------------------------------------------------------------------------- 1 | # Platform landing zone - Full single-region for NVA (Network Virtual Appliance) example 2 | 3 | This example configuration deploys a full single-region landing zone ready for NVA support: 4 | 5 | - Management Groups 6 | - Policy 7 | - Management Resources 8 | - Hub Networking (with Hub and Spoke VNet or vWAN) 9 | - Private DNS Zones for Private Link 10 | - DDOS Protection Plan 11 | 12 | ## Options 13 | 14 | There are two options for deploying the hub networking: 15 | 16 | - Hub and Spoke VNet: [hub-and-spoke-vnet.tfvars](./hub-and-spoke-vnet.tfvars) 17 | - Virtual WAN: [virtual-wan.tfvars](./virtual-wan.tfvars) 18 | 19 | ## Limitations 20 | 21 | The vWAN module does not currently support routes, we'll need to add that per this issue: 22 | 23 | ## Documentation 24 | 25 | The full documentation for this example can be found over at our [Azure Landing Zones documentation site](https://azure.github.io/Azure-Landing-Zones/accelerator/startermodules/terraform-platform-landing-zone/scenarios/). 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ## Overview/Summary 3 | 4 | Replace this with a brief description of what this Pull Request fixes, changes, etc. 5 | 6 | ## This PR fixes/adds/changes/removes 7 | 8 | 1. *Replace me* 9 | 2. *Replace me* 10 | 3. *Replace me* 11 | 12 | ### Breaking Changes 13 | 14 | 1. *Replace me* 15 | 2. *Replace me* 16 | 17 | ## Testing Evidence 18 | 19 | Please provide any testing evidence to show that your Pull Request works/fixes as described and planned (include screenshots, if appropriate). 20 | 21 | ## As part of this Pull Request I have 22 | 23 | - [ ] Checked for duplicate [Pull Requests](https://github.com/Azure/alz-terraform-accelerator/pulls) 24 | - [ ] Associated it with relevant [issues](https://github.com/Azure/alz-terraform-accelerator/issues), for tracking and closure. 25 | - [ ] Ensured my code/branch is up-to-date with the latest changes in the `main` [branch](https://github.com/Azure/alz-terraform-accelerator/tree/main) 26 | - [ ] Performed testing and provided evidence. 27 | - [ ] Updated relevant and associated documentation. 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | **/.terraform.lock.hcl 8 | 9 | # Crash log files 10 | crash.log 11 | 12 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 13 | # .tfvars files are managed as part of configuration and so should be included in 14 | # version control. 15 | # 16 | # example.tfvars 17 | 18 | # Ignore override files as they are usually used to override resources locally and so 19 | # are not checked in 20 | override.tf 21 | override.tf.json 22 | *_override.tf 23 | *_override.tf.json 24 | 25 | # Include override files you do wish to add to version control using negated pattern 26 | # 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | .terraform.lock.hcl 32 | terraform.log 33 | !terraform.tfvars 34 | templates/basic/terraform.tfvars 35 | .vscode/settings.json 36 | .vs 37 | .alzlib 38 | terraform.tfvars 39 | tfplan 40 | templates/platform_landing_zone/examples/test/* 41 | tfplan.txt 42 | test.auto.tfvars 43 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/migration/lib/role_definitions/application_owners.alz_role_definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "8a60c97f-9cb6-536b-b5db-9c997ee1de03", 3 | "type": "Microsoft.Authorization/roleDefinitions", 4 | "apiVersion": "2018-01-01-preview", 5 | "properties": { 6 | "roleName": "[ALZ] Application-Owners", 7 | "description": "Contributor role granted for application/operations team at resource group level", 8 | "type": "CustomRole", 9 | "permissions": [ 10 | { 11 | "actions": [ 12 | "*" 13 | ], 14 | "notActions": [ 15 | "Microsoft.Authorization/*/write", 16 | "Microsoft.Network/publicIPAddresses/write", 17 | "Microsoft.Network/virtualNetworks/write", 18 | "Microsoft.KeyVault/locations/deletedVaults/purge/action" 19 | ], 20 | "dataActions": [], 21 | "notDataActions": [] 22 | } 23 | ], 24 | "assignableScopes": [ 25 | "${current_scope_resource_id}" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/migration/lib/role_definitions/subscription_owner.alz_role_definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e7b246a7-3898-5d30-bc02-dc6c43eae270", 3 | "type": "Microsoft.Authorization/roleDefinitions", 4 | "apiVersion": "2018-01-01-preview", 5 | "properties": { 6 | "roleName": "[ALZ] Subscription-Owner", 7 | "description": "Delegated role for subscription owner generated from subscription Owner role", 8 | "type": "CustomRole", 9 | "permissions": [ 10 | { 11 | "actions": [ 12 | "*" 13 | ], 14 | "notActions": [ 15 | "Microsoft.Authorization/*/write", 16 | "Microsoft.Network/vpnGateways/*", 17 | "Microsoft.Network/expressRouteCircuits/*", 18 | "Microsoft.Network/routeTables/write", 19 | "Microsoft.Network/vpnSites/*" 20 | ], 21 | "dataActions": [], 22 | "notDataActions": [] 23 | } 24 | ], 25 | "assignableScopes": [ 26 | "${current_scope_resource_id}" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /templates/.config/ALZ-Powershell.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "starter_modules": { 3 | "platform_landing_zone": { 4 | "location": "platform_landing_zone", 5 | "short_name": "Azure Verified Modules for Platform Landing Zone (ALZ)", 6 | "description": "Complete Azure Platform Landing Zones Configurable Deployment with Multi-Region Support using Azure Verified Modules" 7 | }, 8 | "test": { 9 | "location": "test", 10 | "short_name": "Test", 11 | "description": "Test Deployment (Warning: Does not deploy an Azure Landing Zone)" 12 | }, 13 | "test_nested": { 14 | "location": "test_nested", 15 | "short_name": "Test Nested", 16 | "description": "Test Nested Submodule Deployment (Warning: Does not deploy an Azure Landing Zone)", 17 | "root_module_folder": "test_root", 18 | "additional_retained_folders": [ 19 | "modules" 20 | ] 21 | }, 22 | "empty": { 23 | "location": "empty", 24 | "short_name": "Empty", 25 | "description": "Empty starter module. Manually add Azure Verified Modules for ALZ." 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /.github/workflows/fmt-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: fmt test 3 | 4 | on: 5 | pull_request: 6 | types: ['opened', 'reopened', 'synchronize'] 7 | merge_group: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: docsfmttest-${{ github.event.pull_request.head.repo.full_name }}/${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | docsfmttest: 16 | name: fmt test 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | 22 | - name: Setup Terraform 23 | uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 24 | with: 25 | terraform_version: latest 26 | terraform_wrapper: false 27 | 28 | - name: Check fmt 29 | run: | 30 | echo "==> Running make fmt" 31 | make fmt 32 | echo "==> Testing for changes to tracked files" 33 | CHANGES=$(git status -suno) 34 | if [ "$CHANGES" ]; then 35 | echo "Repository formatting or documentation is not correct." 36 | echo "Run 'make fmt' locally and commit the changes to fix." 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/management_groups/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | policy_default_values = { for k, v in try(var.management_group_settings.policy_default_values, {}) : k => jsonencode({ value = v }) } 3 | policy_assignments_to_modify = { for management_group_key, management_group_value in try(var.management_group_settings.policy_assignments_to_modify, {}) : management_group_key => { 4 | policy_assignments = { for policy_assignment_key, policy_assignment_value in try(management_group_value.policy_assignments, {}) : policy_assignment_key => { 5 | enforcement_mode = try(policy_assignment_value.enforcement_mode, null) 6 | identity = try(policy_assignment_value.identity, null) 7 | identity_ids = try(policy_assignment_value.identity_ids, null) 8 | parameters = try({ for parameter_key, parameter_value in try(policy_assignment_value.parameters, {}) : parameter_key => jsonencode({ value = parameter_value }) }, null) 9 | non_compliance_messages = try(policy_assignment_value.non_compliance_messages, null) 10 | resource_selectors = try(policy_assignment_value.resource_selectors, null) 11 | overrides = try(policy_assignment_value.overrides, null) 12 | } } 13 | } } 14 | } 15 | -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################### 3 | ########################### 4 | ## Markdown Linter rules ## 5 | ########################### 6 | ########################### 7 | 8 | # Linter rules doc: 9 | # - https://github.com/DavidAnson/markdownlint 10 | # 11 | # Note: 12 | # To comment out a single error: 13 | # 14 | # any violations you want 15 | # 16 | # 17 | 18 | ############### 19 | # Rules by id # 20 | ############### 21 | MD004: false # ul-style - Unordered list style 22 | MD007: 23 | indent: 2 # ul-indent - Unordered list indentation 24 | MD013: 25 | line_length: 400 # line-length - Line length 26 | MD026: 27 | punctuation: ".,;:!。,;:" # no-trailing-punctuation - Trailing punctuation in heading 28 | MD029: false # ol-prefix - Ordered list item prefix 29 | MD033: false # no-inline-html - Inline HTML 30 | MD036: false # no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading 31 | MD041: false # first-line-heading/first-line-h1 - First line in a file should be a top-level heading 32 | 33 | ################# 34 | # Rules by tags # 35 | ################# 36 | blank_lines: false # MD012, MD022, MD031, MD032, MD047 37 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/variables.connectivity.tf: -------------------------------------------------------------------------------- 1 | variable "connectivity_type" { 2 | type = string 3 | description = "The type of network connectivity technology to use for the private DNS zones" 4 | default = "hub_and_spoke_vnet" 5 | validation { 6 | condition = contains(values(local.const.connectivity), var.connectivity_type) 7 | error_message = "The connectivity type must be either 'hub_and_spoke_vnet', 'virtual_wan' or 'none'" 8 | } 9 | } 10 | 11 | variable "connectivity_resource_groups" { 12 | type = map(object({ 13 | name = string 14 | location = string 15 | tags = optional(map(string)) 16 | settings = optional(any) 17 | })) 18 | default = {} 19 | description = <NOTE: The module can be used independently if needed. Example `tfvars` files can be found in the `examples` directory for that use case. 22 | 23 | ### Running Directly 24 | 25 | #### Run the local examples 26 | 27 | Create a `terraform.tfvars` file in the root of the module directory with the following content, replacing the placeholders with the actual values: 28 | 29 | ```hcl 30 | starter_locations = ["uksouth", "ukwest"] 31 | subscription_id_connectivity = "00000000-0000-0000-0000-000000000000" 32 | subscription_id_identity = "00000000-0000-0000-0000-000000000000" 33 | subscription_id_management = "00000000-0000-0000-0000-000000000000" 34 | ``` 35 | 36 | ##### Hub and Spoke Virtual Networks Multi Region 37 | 38 | ```powershell 39 | terraform init 40 | terraform apply -var-file ./examples/full-multi-region/hub-and-spoke-vnet.tfvars 41 | ``` 42 | 43 | ##### Virtual WAN Multi Region 44 | 45 | ```powershell 46 | terraform init 47 | terraform apply -var-file ./examples/full-multi-region/virtual-wan.tfvars 48 | ``` 49 | -------------------------------------------------------------------------------- /templates/empty/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.12" 3 | required_providers { 4 | alz = { 5 | source = "Azure/alz" 6 | version = "0.18.0" 7 | } 8 | azurerm = { 9 | source = "hashicorp/azurerm" 10 | version = "~> 4.0" 11 | } 12 | azapi = { 13 | source = "Azure/azapi" 14 | version = "~> 2.0" 15 | } 16 | local = { 17 | source = "hashicorp/local" 18 | version = "~> 2.5" 19 | } 20 | random = { 21 | source = "hashicorp/random" 22 | version = "~> 3.5" 23 | } 24 | } 25 | # backend "azurerm" {} 26 | } 27 | 28 | 29 | provider "azurerm" { 30 | resource_provider_registrations = "none" 31 | features { 32 | resource_group { 33 | prevent_deletion_if_contains_resources = false 34 | } 35 | } 36 | } 37 | 38 | provider "azurerm" { 39 | resource_provider_registrations = "none" 40 | alias = "management" 41 | subscription_id = var.subscription_ids["management"] 42 | features { 43 | resource_group { 44 | prevent_deletion_if_contains_resources = false 45 | } 46 | } 47 | } 48 | 49 | provider "azurerm" { 50 | resource_provider_registrations = "none" 51 | alias = "connectivity" 52 | subscription_id = var.subscription_ids["connectivity"] 53 | features { 54 | resource_group { 55 | prevent_deletion_if_contains_resources = false 56 | } 57 | } 58 | } 59 | 60 | provider "azurerm" { 61 | resource_provider_registrations = "none" 62 | alias = "identity" 63 | subscription_id = var.subscription_ids["identity"] 64 | features {} 65 | } 66 | 67 | provider "azurerm" { 68 | resource_provider_registrations = "none" 69 | alias = "security" 70 | subscription_id = var.subscription_ids["security"] 71 | features {} 72 | } 73 | 74 | provider "azapi" { 75 | skip_provider_registration = true 76 | } 77 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.12" 3 | required_providers { 4 | alz = { 5 | source = "Azure/alz" 6 | version = "0.20.0" 7 | } 8 | azurerm = { 9 | source = "hashicorp/azurerm" 10 | version = "~> 4.0" 11 | } 12 | azapi = { 13 | source = "Azure/azapi" 14 | version = "~> 2.0" 15 | } 16 | local = { 17 | source = "hashicorp/local" 18 | version = "~> 2.5" 19 | } 20 | } 21 | # backend "azurerm" {} 22 | } 23 | 24 | provider "alz" { 25 | library_overwrite_enabled = true 26 | library_references = [ 27 | { 28 | custom_url = "${path.root}/lib" 29 | } 30 | ] 31 | } 32 | 33 | provider "azapi" { 34 | skip_provider_registration = true 35 | subscription_id = try(var.subscription_ids["management"], var.subscription_id_management) 36 | } 37 | 38 | provider "azurerm" { 39 | resource_provider_registrations = "none" 40 | features { 41 | resource_group { 42 | prevent_deletion_if_contains_resources = false 43 | } 44 | } 45 | } 46 | 47 | provider "azurerm" { 48 | resource_provider_registrations = "none" 49 | alias = "management" 50 | subscription_id = try(var.subscription_ids["management"], var.subscription_id_management) 51 | features { 52 | resource_group { 53 | prevent_deletion_if_contains_resources = false 54 | } 55 | } 56 | } 57 | 58 | provider "azurerm" { 59 | resource_provider_registrations = "none" 60 | alias = "connectivity" 61 | subscription_id = try(var.subscription_ids["connectivity"], var.subscription_id_connectivity) 62 | features { 63 | resource_group { 64 | prevent_deletion_if_contains_resources = false 65 | } 66 | } 67 | } 68 | 69 | provider "azapi" { 70 | alias = "connectivity" 71 | skip_provider_registration = true 72 | subscription_id = try(var.subscription_ids["connectivity"], var.subscription_id_connectivity) 73 | } 74 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/bootstrap/inputs-github.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Required Inputs 3 | 4 | # This section contains the required inputs to bootstrap the Platform Landing Zone 5 | # For more detail on these inputs, visit: https://aka.ms/alz/acc/phase0 6 | 7 | # For advanced configuration options, any variable available in the bootstrap module can be set in this file 8 | # You can find them here: https://aka.ms/alz/acc/bootstrap/github/variables 9 | 10 | ## Decision 4: Bootstrap Resource Azure Region 11 | bootstrap_location: "" 12 | 13 | ## Decision 6: Parent Management Group 14 | root_parent_management_group_id: "" # Leave empty to use Tenant Root Group 15 | 16 | ## Decision 7: Platform Subscriptions 17 | subscription_ids: 18 | management: "" 19 | identity: "" 20 | connectivity: "" 21 | security: "" 22 | 23 | ## Decision 8: Bootstrap Resource Subscription 24 | bootstrap_subscription_id: "" # Leave empty to use the subscription connected to Azure CLI 25 | 26 | ## Decision 9: Bootstrap Resource Naming. 27 | # Default names can be found here: https://aka.ms/alz/acc/bootstrap/github/resourcenames 28 | # For fully custom naming see the FAQ here: https://aka.ms/alz/acc/faq/bootstrap 29 | service_name: "alz" 30 | environment_name: "mgmt" 31 | postfix_number: 1 32 | 33 | ## Decision 10: Bootstrap Networking and Agents 34 | use_self_hosted_agents: true 35 | use_private_networking: true 36 | 37 | ## Decision 11: Version Control System Settings 38 | github_personal_access_token: "" # Can also be supplied via environment variable TF_VAR_github_personal_access_token 39 | github_runners_personal_access_token: "" # Can also be supplied via environment variable TF_VAR_github_runners_personal_access_token 40 | github_organization_name: "" 41 | apply_approvers: [""] 42 | 43 | # Basic Inputs (Do not modify) 44 | iac_type: "terraform" 45 | bootstrap_module_name: "alz_github" 46 | starter_module_name: "platform_landing_zone" 47 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | const = { 3 | connectivity = { 4 | virtual_wan = "virtual_wan" 5 | hub_and_spoke_vnet = "hub_and_spoke_vnet" 6 | none = "none" 7 | } 8 | } 9 | } 10 | 11 | locals { 12 | connectivity_enabled = var.connectivity_type != local.const.connectivity.none 13 | connectivity_virtual_wan_enabled = var.connectivity_type == local.const.connectivity.virtual_wan 14 | connectivity_hub_and_spoke_vnet_enabled = var.connectivity_type == local.const.connectivity.hub_and_spoke_vnet 15 | } 16 | 17 | # Build an implicit dependency on the resource groups 18 | locals { 19 | resource_groups = { 20 | resource_groups = module.resource_groups 21 | } 22 | hub_and_spoke_networks_settings = merge(module.config.outputs.hub_and_spoke_networks_settings, local.resource_groups) 23 | hub_virtual_networks = (merge({ vnets = module.config.outputs.hub_virtual_networks }, local.resource_groups)).vnets 24 | virtual_wan_settings = merge(module.config.outputs.virtual_wan_settings, local.resource_groups) 25 | virtual_hubs = (merge({ vhubs = module.config.outputs.virtual_hubs }, local.resource_groups)).vhubs 26 | } 27 | 28 | # Build policy dependencies 29 | locals { 30 | management_group_dependencies = { 31 | policy_assignments = [ 32 | module.management_resources, 33 | module.hub_and_spoke_vnet, 34 | module.virtual_wan 35 | ] 36 | policy_role_assignments = [ 37 | module.management_resources, 38 | module.hub_and_spoke_vnet, 39 | module.virtual_wan 40 | ] 41 | } 42 | } 43 | 44 | locals { 45 | management_group_settings = merge( 46 | module.config.outputs.management_group_settings, 47 | { 48 | dependencies = local.management_group_dependencies 49 | } 50 | ) 51 | management_resource_settings = merge( 52 | module.config.outputs.management_resource_settings, 53 | { 54 | tags = coalesce(module.config.outputs.management_resource_settings.tags, module.config.outputs.tags) 55 | } 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/bootstrap/inputs-local.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Required Inputs 3 | 4 | # This section contains the required inputs to bootstrap the Platform Landing Zone 5 | # For more detail on these inputs, visit: https://aka.ms/alz/acc/phase0 6 | 7 | # For advanced configuration options, any variable available in the bootstrap module can be set in this file 8 | # You can find them here: https://aka.ms/alz/acc/bootstrap/local/variables 9 | 10 | ## Decision 4: Bootstrap Resource Azure Region 11 | bootstrap_location: "" 12 | 13 | ## Decision 6: Parent Management Group 14 | root_parent_management_group_id: "" # Leave empty to use Tenant Root Group 15 | 16 | ## Decision 7: Platform Subscriptions 17 | subscription_ids: 18 | management: "" 19 | identity: "" 20 | connectivity: "" 21 | security: "" 22 | 23 | ## Decision 8: Bootstrap Resource Subscription 24 | bootstrap_subscription_id: "" # Leave empty to use the subscription connected to Azure CLI 25 | 26 | ## Decision 9: Bootstrap Resource Naming. 27 | # Default names can be found here: https://aka.ms/alz/acc/bootstrap/local/resourcenames 28 | # For fully custom naming see the FAQ here: https://aka.ms/alz/acc/faq/bootstrap 29 | service_name: "alz" 30 | environment_name: "mgmt" 31 | postfix_number: 1 32 | 33 | # Local Specific Inputs 34 | 35 | ## Whether to create bootstrap resource in Azure 36 | # This is not required if you plan to setup you own Identities, etc. 37 | create_bootstrap_resources_in_azure: true 38 | 39 | ## Whether to grant permissions for the current Azure CLI user to be able to deploy the Platform Landing Zones 40 | # This is not required if you plan to configure a third-party Version Control System 41 | grant_permissions_to_current_user: true 42 | 43 | ## Target Directory for generated files 44 | # Leave empty to use the standard output directory 45 | target_directory: "" 46 | 47 | # Basic Inputs (Do not modify) 48 | iac_type: "terraform" 49 | bootstrap_module_name: "alz_local" 50 | starter_module_name: "platform_landing_zone" 51 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/bootstrap/inputs-azure-devops.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Required Inputs 3 | 4 | # This section contains the required inputs to bootstrap the Platform Landing Zone 5 | # For more detail on these inputs, visit: https://aka.ms/alz/acc/phase0 6 | 7 | # For advanced configuration options, any variable available in the bootstrap module can be set in this file 8 | # You can find them here: https://aka.ms/alz/acc/bootstrap/azuredevops/variables 9 | 10 | ## Decision 4: Bootstrap Resource Azure Region 11 | bootstrap_location: "" 12 | 13 | ## Decision 6: Parent Management Group 14 | root_parent_management_group_id: "" # Leave empty to use Tenant Root Group 15 | 16 | ## Decision 7: Platform Subscriptions 17 | subscription_ids: 18 | management: "" 19 | identity: "" 20 | connectivity: "" 21 | security: "" 22 | 23 | ## Decision 8: Bootstrap Resource Subscription 24 | bootstrap_subscription_id: "" # Leave empty to use the subscription connected to Azure CLI 25 | 26 | ## Decision 9: Bootstrap Resource Naming. 27 | # Default names can be found here: https://aka.ms/alz/acc/bootstrap/azuredevops/resourcenames 28 | # For fully custom naming see the FAQ here: https://aka.ms/alz/acc/faq/bootstrap 29 | service_name: "alz" 30 | environment_name: "mgmt" 31 | postfix_number: 1 32 | 33 | ## Decision 10: Bootstrap Networking and Agents 34 | use_self_hosted_agents: true 35 | use_private_networking: true 36 | 37 | ## Decision 11: Version Control System Settings 38 | azure_devops_personal_access_token: "" # Can also be supplied via environment variable TF_VAR_azure_devops_personal_access_token 39 | azure_devops_agents_personal_access_token: "" # Can also be supplied via environment variable TF_VAR_azure_devops_agents_personal_access_token 40 | azure_devops_organization_name: "" 41 | azure_devops_project_name: "" 42 | apply_approvers: [""] 43 | 44 | # Basic Inputs (Do not modify) 45 | iac_type: "terraform" 46 | bootstrap_module_name: "alz_azuredevops" 47 | starter_module_name: "platform_landing_zone" 48 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/config-templating/variables.tf: -------------------------------------------------------------------------------- 1 | variable "starter_locations" { 2 | type = list(string) 3 | description = "The default for Azure resources. (e.g 'uksouth')" 4 | } 5 | 6 | variable "starter_locations_short" { 7 | type = map(string) 8 | default = {} 9 | description = "Optional overrides for starter location short codes. Keys should match the built-in replacement names (for example 'starter_location_01_short', 'starter_location_02_short')." 10 | } 11 | 12 | variable "subscription_id_connectivity" { 13 | type = string 14 | description = "value of the subscription id for the Connectivity subscription" 15 | } 16 | 17 | variable "subscription_id_identity" { 18 | type = string 19 | description = "value of the subscription id for the Identity subscription" 20 | } 21 | 22 | variable "subscription_id_management" { 23 | type = string 24 | description = "value of the subscription id for the Management subscription" 25 | } 26 | 27 | variable "subscription_id_security" { 28 | type = string 29 | description = "value of the subscription id for the Security subscription" 30 | } 31 | 32 | variable "root_parent_management_group_id" { 33 | type = string 34 | default = "" 35 | description = "This is the id of the management group that the ALZ hierarchy will be nested under, will default to the Tenant Root Group" 36 | } 37 | 38 | variable "custom_replacements" { 39 | type = object({ 40 | names = optional(map(string), {}) 41 | resource_group_identifiers = optional(map(string), {}) 42 | resource_identifiers = optional(map(string), {}) 43 | }) 44 | description = "Custom replacements" 45 | } 46 | 47 | variable "inputs" { 48 | type = any 49 | description = "A map of input variables to be used in the configuration templating module." 50 | } 51 | 52 | variable "enable_telemetry" { 53 | type = bool 54 | default = true 55 | description = <. 58 | If it is set to false, then no telemetry will be collected. 59 | DESCRIPTION 60 | nullable = false 61 | } -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/management_groups/main.tf: -------------------------------------------------------------------------------- 1 | module "management_groups" { 2 | source = "Azure/avm-ptn-alz/azurerm" 3 | version = "0.14.1" 4 | architecture_name = var.management_group_settings.architecture_name 5 | parent_resource_id = var.management_group_settings.parent_resource_id 6 | location = var.management_group_settings.location 7 | policy_default_values = local.policy_default_values 8 | policy_assignments_to_modify = local.policy_assignments_to_modify 9 | enable_telemetry = var.enable_telemetry 10 | management_group_hierarchy_settings = var.management_group_settings.management_group_hierarchy_settings 11 | partner_id = var.management_group_settings.partner_id 12 | retries = var.management_group_settings.retries 13 | subscription_placement = var.management_group_settings.subscription_placement 14 | timeouts = var.management_group_settings.timeouts 15 | dependencies = var.management_group_settings.dependencies 16 | override_policy_definition_parameter_assign_permissions_set = var.management_group_settings.override_policy_definition_parameter_assign_permissions_set 17 | override_policy_definition_parameter_assign_permissions_unset = var.management_group_settings.override_policy_definition_parameter_assign_permissions_unset 18 | management_group_role_assignments = var.management_group_settings.management_group_role_assignments 19 | role_assignment_definition_lookup_enabled = var.management_group_settings.role_assignment_definition_lookup_enabled 20 | policy_assignment_non_compliance_message_settings = var.management_group_settings.policy_assignment_non_compliance_message_settings 21 | role_assignment_name_use_random_uuid = var.management_group_settings.role_assignment_name_use_random_uuid 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/wiki-sync.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docs/Wiki Sync 3 | 4 | permissions: 5 | contents: write 6 | 7 | # yamllint disable-line rule:truthy 8 | on: 9 | release: 10 | types: [published] 11 | workflow_dispatch: 12 | 13 | env: 14 | wiki_source_repo: "${{ github.repository }}" 15 | wiki_source_repo_dir: "${{ github.repository }}/docs/wiki" 16 | wiki_target_repo: "${{ github.repository }}.wiki" 17 | github_user_name: "github-actions" 18 | github_email: "github-actions@github.com" 19 | github_commit_message: "GitHub Action syncing wiki from docs/wiki" 20 | 21 | jobs: 22 | sync-wiki: 23 | name: Sync Wiki 24 | if: github.repository == 'Azure/alz-terraform-accelerator' || github.event_name == 'workflow_dispatch' 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout Source Repo 28 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 29 | with: 30 | repository: ${{ env.wiki_source_repo }} 31 | path: ${{ env.wiki_source_repo }} 32 | 33 | - name: Checkout Wiki Repo 34 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 35 | with: 36 | repository: ${{ env.wiki_target_repo }} 37 | path: ${{ env.wiki_target_repo }} 38 | 39 | - name: Configure Local Git 40 | run: | 41 | git config --global user.name "$github_user_name" 42 | git config --global user.email "$github_email" 43 | working-directory: ${{ env.GITHUB_WORKSPACE }} 44 | 45 | - name: Sync docs/wiki Into Wiki Repo 46 | run: | 47 | rsync -avzr --delete --exclude='.git/' "$wiki_source_repo_dir/" "$wiki_target_repo" 48 | working-directory: ${{ env.GITHUB_WORKSPACE }} 49 | 50 | - name: Check for changes 51 | id: git_status 52 | run: | 53 | mapfile -t "CHECK_GIT_STATUS" < <(git status -s) 54 | printf "%s\n" "${CHECK_GIT_STATUS[@]}" 55 | echo "changes=${#CHECK_GIT_STATUS[@]}" >> "$GITHUB_OUTPUT" 56 | working-directory: ${{ env.wiki_target_repo }} 57 | 58 | - name: Add files, commit and push into Wiki 59 | if: steps.git_status.outputs.changes > 0 60 | run: | 61 | echo "Pushing changes to origin..." 62 | git add . 63 | git commit -m "$github_commit_message [$GITHUB_ACTOR/${GITHUB_SHA::8}]" 64 | git push --set-upstream "https://$GITHUB_TOKEN@github.com/$wiki_target_repo.git" master 65 | working-directory: ${{ env.wiki_target_repo }} 66 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/slz/lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml: -------------------------------------------------------------------------------- 1 | name: alz_custom 2 | management_groups: 3 | - id: alz 4 | display_name: Sovereign Landing Zone 5 | archetypes: 6 | - root_custom 7 | - sovereign_root_custom 8 | exists: false 9 | parent_id: null 10 | 11 | - id: platform 12 | display_name: Platform 13 | archetypes: 14 | - platform_custom 15 | exists: false 16 | parent_id: alz 17 | 18 | - id: landingzones 19 | display_name: Landing Zones 20 | archetypes: 21 | - landing_zones_custom 22 | exists: false 23 | parent_id: alz 24 | 25 | - id: public 26 | display_name: Public 27 | archetypes: 28 | - public_custom 29 | exists: false 30 | parent_id: landingzones 31 | 32 | - id: corp 33 | display_name: Corp 34 | archetypes: 35 | - corp_custom 36 | exists: false 37 | parent_id: landingzones 38 | 39 | - id: online 40 | display_name: Online 41 | archetypes: 42 | - online_custom 43 | exists: false 44 | parent_id: landingzones 45 | 46 | - id: confidential-corp 47 | display_name: Confidential Corp 48 | archetypes: 49 | - confidential_corp_custom 50 | exists: false 51 | parent_id: landingzones 52 | 53 | - id: confidential-online 54 | display_name: Confidential Online 55 | archetypes: 56 | - confidential_online_custom 57 | exists: false 58 | parent_id: landingzones 59 | 60 | - id: sandbox 61 | display_name: Sandbox 62 | archetypes: 63 | - sandbox_custom 64 | exists: false 65 | parent_id: alz 66 | 67 | - id: security 68 | display_name: Security 69 | archetypes: 70 | - security_custom 71 | exists: false 72 | parent_id: platform 73 | 74 | - id: management 75 | display_name: Management 76 | archetypes: 77 | - management_custom 78 | exists: false 79 | parent_id: platform 80 | 81 | - id: connectivity 82 | display_name: Connectivity 83 | archetypes: 84 | - connectivity_custom 85 | exists: false 86 | parent_id: platform 87 | 88 | - id: identity 89 | display_name: Identity 90 | archetypes: 91 | - identity_custom 92 | exists: false 93 | parent_id: platform 94 | 95 | - id: decommissioned 96 | display_name: Decommissioned 97 | archetypes: 98 | - decommissioned_custom 99 | exists: false 100 | parent_id: alz 101 | -------------------------------------------------------------------------------- /templates/empty/README.md: -------------------------------------------------------------------------------- 1 | # Azure Landing Zones Accelerator Starter Module for Terraform - Empty 2 | 3 | This module is part of the Azure Landing Zones Accelerator solution. This configuration includes a minimal set of files (providers and GitHub Actions workflows) to get you started and then give you the option to create your own configuration. 4 | 5 | ## AVM Modules 6 | 7 | You may reference the [Azure Landing Zone AVM modules](https://registry.terraform.io/search/modules?q=Azure%2Favm-ptn-alz) to build out your own configuration. 8 | 9 | Each module has a drop down of examples to use as a starting point. For example, the [management example](https://registry.terraform.io/modules/Azure/avm-ptn-alz/azurerm/latest/examples/management) for the core Azure Landing Zone pattern module. 10 | 11 | If you would like to get started based on this example then follow the [quickstart](./QUICKSTART.md). 12 | 13 | ## Azure Landing Zone libraries 14 | 15 | ### The alz provider 16 | 17 | The [alz provider](https://registry.terraform.io/providers/Azure/alz/latest/docs) creates the Azure Landing Zone library used by the main alz module to define assets such as: 18 | 19 | - role definitions 20 | - policy definitions 21 | - policy initiative definitions 22 | - policy assignments 23 | 24 | These are then grouped using the following constructs 25 | 26 | - archetypes, which are grouping of the assets above 27 | - archetype overrides, which is a delta from an archetype 28 | - architectures, representing the management group structure and assigned archetypes and overrides 29 | - metadata (including dependency references to any other libraries) and default values for policies 30 | 31 | ### Microsoft maintained libraries 32 | 33 | Microsoft maintains a structured set of versioned releases of libraries for Azure Landing Zone, Sovereign Landing Zone and more. Go to to see the most recent releases and changelog. 34 | 35 | Example alz provider block using an official release: 36 | 37 | ```ruby 38 | provider "alz" { 39 | library_references = [ 40 | { 41 | path = "platform/alz" 42 | ref = "2025.02.0" 43 | } 44 | ] 45 | } 46 | ``` 47 | 48 | This will pull the alz library from . 49 | 50 | ### Custom libraries 51 | 52 | The alz provider and library format are designed to allow customization and extensibility. You can override the Microsoft libraries, modify assignments, create your own library of assets, extend or replace, and more. 53 | 54 | Refer to the documentation. 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Landing Zones Terraform Accelerator 2 | 3 | [![End to End Tests](https://github.com/Azure/alz-terraform-accelerator/actions/workflows/end-to-end-test.yml/badge.svg)](https://github.com/Azure/alz-terraform-accelerator/actions/workflows/end-to-end-test.yml) 4 | [![license](https://img.shields.io/badge/License-MIT-purple.svg)](LICENSE) 5 | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Azure/alz-terraform-accelerator/badge)](https://scorecard.dev/viewer/?uri=github.com/Azure/alz-terraform-accelerator) 6 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/Azure/alz-terraform-accelerator?style=flat&logo=github) 7 | ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Azure/alz-terraform-accelerator) 8 | ![GitHub contributors](https://img.shields.io/github/contributors/Azure/alz-terraform-accelerator) 9 | 10 | ## Introduction 11 | 12 | This repository provides the starter modules for the Azure Landing Zones IaC Terraform Accelerator. 13 | 14 | Please head over to [aka.ms/alz/acc](https://aka.ms/alz/acc) for detailed features and usage instructions. 15 | 16 | ## Issues 17 | 18 | Please raise an issue over here: [https://aka.ms/alz/acc/issues](https://aka.ms/alz/acc/issues) 19 | 20 | ## Contributing 21 | 22 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 23 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 24 | the rights to use your contribution. For details, visit [https://cla.opensource.microsoft.com](https://cla.opensource.microsoft.com). 25 | 26 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 27 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 28 | provided by the bot. You will only need to do this once across all repos using our CLA. 29 | 30 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 31 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 32 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 33 | 34 | ## Trademarks 35 | 36 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 37 | trademarks or logos is subject to and must follow 38 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). 39 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 40 | Any use of third-party trademarks or logos are subject to those third-party's policies. 41 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/management_resources/main.tf: -------------------------------------------------------------------------------- 1 | module "management_resources" { 2 | source = "Azure/avm-ptn-alz-management/azurerm" 3 | version = "0.9.0" 4 | automation_account_name = null 5 | location = var.management_resource_settings.location 6 | log_analytics_workspace_name = coalesce(var.management_resource_settings.log_analytics_workspace_name, "law-management-${var.management_resource_settings.location}") 7 | resource_group_name = coalesce(var.management_resource_settings.resource_group_name, "rg-management-${var.management_resource_settings.location}") 8 | data_collection_rules = var.management_resource_settings.data_collection_rules 9 | enable_telemetry = var.enable_telemetry 10 | linked_automation_account_creation_enabled = false 11 | log_analytics_solution_plans = var.management_resource_settings.log_analytics_solution_plans 12 | log_analytics_workspace_allow_resource_only_permissions = var.management_resource_settings.log_analytics_workspace_allow_resource_only_permissions 13 | log_analytics_workspace_cmk_for_query_forced = var.management_resource_settings.log_analytics_workspace_cmk_for_query_forced 14 | log_analytics_workspace_daily_quota_gb = var.management_resource_settings.log_analytics_workspace_daily_quota_gb 15 | log_analytics_workspace_internet_ingestion_enabled = var.management_resource_settings.log_analytics_workspace_internet_ingestion_enabled 16 | log_analytics_workspace_internet_query_enabled = var.management_resource_settings.log_analytics_workspace_internet_query_enabled 17 | log_analytics_workspace_local_authentication_enabled = var.management_resource_settings.log_analytics_workspace_local_authentication_enabled 18 | log_analytics_workspace_reservation_capacity_in_gb_per_day = var.management_resource_settings.log_analytics_workspace_reservation_capacity_in_gb_per_day 19 | log_analytics_workspace_retention_in_days = var.management_resource_settings.log_analytics_workspace_retention_in_days 20 | log_analytics_workspace_sku = var.management_resource_settings.log_analytics_workspace_sku 21 | resource_group_creation_enabled = true 22 | sentinel_onboarding = var.management_resource_settings.sentinel_onboarding 23 | tags = var.management_resource_settings.tags 24 | timeouts = var.management_resource_settings.timeouts 25 | user_assigned_managed_identities = var.management_resource_settings.user_assigned_managed_identities 26 | } 27 | -------------------------------------------------------------------------------- /templates/test/main.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_client_config" "current" {} 2 | data "azurerm_subscription" "current" {} 3 | 4 | data "azurerm_management_group" "example_parent" { 5 | name = var.root_parent_management_group_id == "" ? data.azurerm_client_config.current.tenant_id : var.root_parent_management_group_id 6 | } 7 | 8 | locals { 9 | starter_location = var.starter_locations[0] 10 | } 11 | 12 | resource "random_string" "example" { 13 | length = 10 14 | special = false 15 | numeric = false 16 | upper = false 17 | } 18 | 19 | resource "azurerm_management_group" "example_child" { 20 | name = "e2e-test-${random_string.example.result}" 21 | display_name = "${var.child_management_group_display_name} ${random_string.example.result}" 22 | parent_management_group_id = data.azurerm_management_group.example_parent.id 23 | } 24 | 25 | resource "azurerm_resource_group" "management" { 26 | provider = azurerm.management 27 | name = "e2e-test-management-azurerm-${random_string.example.result}" 28 | location = local.starter_location 29 | } 30 | 31 | resource "azurerm_resource_group" "connectivity" { 32 | provider = azurerm.connectivity 33 | name = "e2e-test-connectivity-azurerm-${random_string.example.result}" 34 | location = local.starter_location 35 | } 36 | 37 | resource "azurerm_resource_group" "identity" { 38 | provider = azurerm.identity 39 | name = "e2e-test-identity-azurerm-${random_string.example.result}" 40 | location = local.starter_location 41 | } 42 | 43 | resource "azurerm_resource_group" "security" { 44 | provider = azurerm.security 45 | name = "e2e-test-security-azurerm-${random_string.example.result}" 46 | location = local.starter_location 47 | } 48 | 49 | resource "azapi_resource" "resource_group_management" { 50 | parent_id = "/subscriptions/${var.subscription_ids["management"]}" 51 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 52 | name = "e2e-test-management-azapi-${random_string.example.result}" 53 | location = local.starter_location 54 | body = { 55 | properties = {} 56 | } 57 | schema_validation_enabled = false 58 | } 59 | 60 | resource "azapi_resource" "resource_group_connectivity" { 61 | parent_id = "/subscriptions/${var.subscription_ids["connectivity"]}" 62 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 63 | name = "e2e-test-connectivity-azapi-${random_string.example.result}" 64 | location = local.starter_location 65 | body = { 66 | properties = {} 67 | } 68 | schema_validation_enabled = false 69 | } 70 | 71 | resource "azapi_resource" "resource_group_identity" { 72 | parent_id = "/subscriptions/${var.subscription_ids["identity"]}" 73 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 74 | name = "e2e-test-identity-azapi-${random_string.example.result}" 75 | location = local.starter_location 76 | body = { 77 | properties = {} 78 | } 79 | schema_validation_enabled = false 80 | } 81 | 82 | resource "azapi_resource" "resource_group_security" { 83 | parent_id = "/subscriptions/${var.subscription_ids["security"]}" 84 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 85 | name = "e2e-test-security-azapi-${random_string.example.result}" 86 | location = local.starter_location 87 | body = { 88 | properties = {} 89 | } 90 | schema_validation_enabled = false 91 | } 92 | -------------------------------------------------------------------------------- /templates/test_nested/test_to_remove/main.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_client_config" "current" {} 2 | data "azurerm_subscription" "current" {} 3 | 4 | data "azurerm_management_group" "example_parent" { 5 | name = var.root_parent_management_group_id == "" ? data.azurerm_client_config.current.tenant_id : var.root_parent_management_group_id 6 | } 7 | 8 | locals { 9 | starter_location = var.starter_locations[0] 10 | } 11 | 12 | resource "random_string" "example" { 13 | length = 10 14 | special = false 15 | numeric = false 16 | upper = false 17 | } 18 | 19 | resource "azurerm_management_group" "example_child" { 20 | name = "e2e-test-${random_string.example.result}" 21 | display_name = "${var.child_management_group_display_name} ${random_string.example.result}" 22 | parent_management_group_id = data.azurerm_management_group.example_parent.id 23 | } 24 | 25 | resource "azurerm_resource_group" "management" { 26 | provider = azurerm.management 27 | name = "e2e-test-management-azurerm-${random_string.example.result}" 28 | location = local.starter_location 29 | } 30 | 31 | resource "azurerm_resource_group" "connectivity" { 32 | provider = azurerm.connectivity 33 | name = "e2e-test-connectivity-azurerm-${random_string.example.result}" 34 | location = local.starter_location 35 | } 36 | 37 | resource "azurerm_resource_group" "identity" { 38 | provider = azurerm.identity 39 | name = "e2e-test-identity-azurerm-${random_string.example.result}" 40 | location = local.starter_location 41 | } 42 | 43 | resource "azurerm_resource_group" "security" { 44 | provider = azurerm.security 45 | name = "e2e-test-security-azurerm-${random_string.example.result}" 46 | location = local.starter_location 47 | } 48 | 49 | resource "azapi_resource" "resource_group_management" { 50 | parent_id = "/subscriptions/${var.subscription_ids["management"]}" 51 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 52 | name = "e2e-test-management-azapi-${random_string.example.result}" 53 | location = local.starter_location 54 | body = { 55 | properties = {} 56 | } 57 | schema_validation_enabled = false 58 | } 59 | 60 | resource "azapi_resource" "resource_group_connectivity" { 61 | parent_id = "/subscriptions/${var.subscription_ids["connectivity"]}" 62 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 63 | name = "e2e-test-connectivity-azapi-${random_string.example.result}" 64 | location = local.starter_location 65 | body = { 66 | properties = {} 67 | } 68 | schema_validation_enabled = false 69 | } 70 | 71 | resource "azapi_resource" "resource_group_identity" { 72 | parent_id = "/subscriptions/${var.subscription_ids["identity"]}" 73 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 74 | name = "e2e-test-identity-azapi-${random_string.example.result}" 75 | location = local.starter_location 76 | body = { 77 | properties = {} 78 | } 79 | schema_validation_enabled = false 80 | } 81 | 82 | resource "azapi_resource" "resource_group_security" { 83 | parent_id = "/subscriptions/${var.subscription_ids["security"]}" 84 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 85 | name = "e2e-test-security-azapi-${random_string.example.result}" 86 | location = local.starter_location 87 | body = { 88 | properties = {} 89 | } 90 | schema_validation_enabled = false 91 | } 92 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This workflow uses actions that are not certified by GitHub. They are provided 3 | # by a third-party and are governed by separate terms of service, privacy 4 | # policy, and support documentation. 5 | 6 | name: Scorecard supply-chain security 7 | on: 8 | # For Branch-Protection check. Only the default branch is supported. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 10 | branch_protection_rule: 11 | # To guarantee Maintained check is occasionally updated. See 12 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 13 | schedule: 14 | - cron: '24 16 * * 1' 15 | push: 16 | branches: ["main"] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | # Uncomment the permissions below if installing in a private repository. 31 | # contents: read 32 | # actions: read 33 | 34 | steps: 35 | - name: "Checkout code" 36 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 42 | with: 43 | results_file: results.sarif 44 | results_format: sarif 45 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 46 | # - you want to enable the Branch-Protection check on a *public* repository, or 47 | # - you are installing Scorecard on a *private* repository 48 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 49 | repo_token: ${{ secrets.SCORECARD_TOKEN }} 50 | 51 | # Public repositories: 52 | # - Publish results to OpenSSF REST API for easy access by consumers 53 | # - Allows the repository to include the Scorecard badge. 54 | # - See https://github.com/ossf/scorecard-action#publishing-results. 55 | # For private repositories: 56 | # - `publish_results` will always be set to `false`, regardless 57 | # of the value entered here. 58 | publish_results: true 59 | 60 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 61 | # format to the repository Actions tab. 62 | - name: "Upload artifact" 63 | uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 64 | with: 65 | name: SARIF file 66 | path: results.sarif 67 | retention-days: 5 68 | 69 | # Upload the results to GitHub's code scanning dashboard (optional). 70 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 71 | - name: "Upload to code-scanning" 72 | uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 73 | with: 74 | sarif_file: results.sarif 75 | -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Developer Requirements 2 | 3 | * [Terraform (Core)](https://www.terraform.io/downloads.html) - version 1.x or above 4 | * [Go](https://golang.org/doc/install) version 1.20.x (to run the tests) 5 | 6 | ## On Windows 7 | 8 | If you're on Windows you'll also need: 9 | 10 | * [Git Bash for Windows](https://git-scm.com/download/win) 11 | * [Make for Windows](http://gnuwin32.sourceforge.net/packages/make.htm) 12 | 13 | For *GNU32 Make*, make sure its bin path is added to PATH environment variable. 14 | 15 | For *Git Bash for Windows*, at the step of "Adjusting your PATH environment", please choose "Use Git and optional Unix tools from Windows Command Prompt". 16 | 17 | Or, use [Windows Subsystem for Linux](https://docs.microsoft.com/windows/wsl/install) 18 | 19 | ### Setup on WSL with Ubuntu 22.04 20 | 21 | #### Install Go and Make 22 | 23 | 1. Run `curl -L https://go.dev/dl/go1.21.0.linux-amd64.tar.gz -o go1.21.0.linux-amd64.tar.gz` (replace with a different version of go if desired) 24 | 2. Run `rm -rf /usr/local/go && tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz` 25 | 3. Run `sudo nano ~/.profile` 26 | 4. Add the following lines at the end of the file: 27 | ```bash 28 | export PATH=$PATH:/usr/local/go/bin 29 | export GOPATH=$HOME/go/bin 30 | export PATH=$PATH:$GOPATH/bin 31 | ``` 32 | 5. Type Ctrl + x to save, then enter y and hit enter 33 | 5. Run `source ~/.profile` 34 | 6. Run `sudo apt-get update && apt-get install make` 35 | 36 | #### Install Terraform 37 | 38 | 1. Follow these instructions `https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli`. 39 | 40 | #### Setup Project Tools 41 | 42 | 1. Clone the repository 43 | 2. Navigate to the root of the repository 44 | 3. Run `make fmt` 45 | 4. Run `make tfclean` 46 | 47 | ## PR Naming 48 | 49 | We have adopted [conventional commit](https://www.conventionalcommits.org/) naming standards for PRs. 50 | 51 | E.g.: 52 | 53 | ```text 54 | feat(roleassignment)!: add `relative_scope` value. 55 | ^ ^ ^ ^ 56 | | | | |__ Subject 57 | | |_____ Scope |____ Breaking change flag 58 | |__________ Type 59 | ``` 60 | 61 | ### Type 62 | 63 | The following types are permitted: 64 | 65 | * `chore` - Other changes that do not modify src or test files 66 | * `ci` - changes to the CI system 67 | * `docs` - documentation only changes 68 | * `feat` - a new feature (this correlates with `MINOR` in Semantic Versioning) 69 | * `fix` - a bug fix (this correlates with `PATCH` in Semantic Versioning) 70 | * `refactor` - a code change that neither fixes a bug or adds a feature 71 | * `revert` - revert to a previous commit 72 | * `style` - changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 73 | * `test` - adding or correcting tests 74 | 75 | ### Scope (Optional) 76 | 77 | The following scopes are permitted: 78 | 79 | * resourcegroup - pertaining to the resourcegroup sub-module 80 | * roleassignment - pertaining to the roleassignment sub-module 81 | * root - pertaining to the root module 82 | * subscription - pertaining to the subscription sub-module 83 | * usermanagedidentity - pertaining to the user-assigned managed identity sub-module 84 | * virtualnetwork - pertaining to the virtual network sub-module 85 | 86 | ### Breaking Changes 87 | 88 | An exclamation mark `!` is appended to the type/scope of a breaking change PR (this correlates with `MAJOR` in Semantic Versioning). 89 | -------------------------------------------------------------------------------- /.github/tests/scripts/generate-matrix.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$runNumber = "999", 3 | [string]$rootModuleFolder = "./templates/platform_landing_zone", 4 | [string]$exampleFolders = "./templates/platform_landing_zone/examples", 5 | [int]$splitCount = 20, 6 | [string]$subscriptionNamePrefix = "alz-acc-avm-test-", 7 | [string]$managementGroupIdPrefix = "alz-acc-avm-test-", 8 | [string]$subscriptionNameSuffixNumberLength = 2, 9 | [string]$managementGroupIdSuffixNumberLength = 2, 10 | [string]$primaryTestMode = "apply", 11 | [string]$secondaryTestMode = "plan", 12 | [switch]$includeSecondaryTests 13 | ) 14 | 15 | $configFiles = Get-ChildItem -Path $exampleFolders -Recurse -Filter "*.tfvars" -Force 16 | $configFiles = $configFiles | Sort-Object -Property Directory, Name 17 | 18 | $matrix = @() 19 | 20 | $configFileNumber = 1 21 | 22 | foreach($configFile in $configFiles) { 23 | $directory = $configFile.Directory.Name 24 | $configFileName = [System.IO.Path]::GetFileNameWithoutExtension($configFile.Name) 25 | 26 | $shortDirectory = [System.String]::Join("", ($directory.Split("-") | ForEach-Object { $_.Substring(0, 1)})) 27 | $shortFileName = [System.String]::Join("", ($configFileName.Split("-") | ForEach-Object { $_.Substring(0, 1)})) 28 | 29 | $subscriptionName = $subscriptionNamePrefix + ("{0:D$subscriptionNameSuffixNumberLength}" -f $configFileNumber) 30 | $managementGroupId = $managementGroupIdPrefix + ("{0:D$managementGroupIdSuffixNumberLength}" -f $configFileNumber) 31 | 32 | $environmentNumberFormatted = "{0:D2}" -f $configFileNumber 33 | 34 | $matrixItem = @{ 35 | name = $directory + "-$environmentNumberFormatted--" + $configFileName + "-" + $runNumber + "-a" 36 | shortName = $shortDirectory + $shortFileName + "-" + $runNumber + "-a" 37 | configFilePath = $configFile.FullName 38 | rootModuleFolderPath = $rootModuleFolder 39 | splitNumber = 1 40 | splitIncrement = 0 41 | mode = $primaryTestMode 42 | subscriptionNameConnectivity = "$subscriptionName-connectivity" 43 | subscriptionNameManagement = "$subscriptionName-management" 44 | subscriptionNameIdentity = "$subscriptionName-identity" 45 | subscriptionNameSecurity = "$subscriptionName-security" 46 | managementGroupId = $managementGroupId 47 | environmentNumber = $configFileNumber 48 | } 49 | $matrix += $matrixItem 50 | 51 | if(!$includeSecondaryTests) { 52 | continue 53 | } 54 | 55 | for ($i = 0; $i -lt $splitCount; $i++) { 56 | $splitNumber = "{0:D2}" -f ($i + 1) 57 | $matrixItem = @{ 58 | name = $directory + "--" + $configFileName + "-" + $runNumber + "-p-" + $splitNumber 59 | shortName = $shortDirectory + $shortFileName + "-" + $runNumber + "-p-" + $splitNumber 60 | configFilePath = $configFile.FullName 61 | rootModuleFolderPath = $rootModuleFolder 62 | splitNumber = $i + 1 63 | splitIncrement = $splitCount 64 | mode = $secondaryTestMode 65 | subscriptionNameConnectivity = "$subscriptionName-connectivity" 66 | subscriptionNameManagement = "$subscriptionName-management" 67 | subscriptionNameIdentity = "$subscriptionName-identity" 68 | subscriptionNameSecurity = "$subscriptionName-security" 69 | managementGroupId = $managementGroupId 70 | environmentNumber = $configFileNumber 71 | } 72 | $matrix += $matrixItem 73 | } 74 | $configFileNumber++ 75 | } 76 | 77 | $matrix = $matrix | Sort-Object -Property mode, splitNumber, name 78 | 79 | return $matrix 80 | -------------------------------------------------------------------------------- /templates/test_nested/test_root/main.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_client_config" "current" {} 2 | data "azurerm_subscription" "current" {} 3 | 4 | data "azurerm_management_group" "example_parent" { 5 | name = var.root_parent_management_group_id == "" ? data.azurerm_client_config.current.tenant_id : var.root_parent_management_group_id 6 | } 7 | 8 | locals { 9 | starter_location = var.starter_locations[0] 10 | } 11 | 12 | resource "random_string" "example" { 13 | length = 10 14 | special = false 15 | numeric = false 16 | upper = false 17 | } 18 | 19 | resource "azurerm_management_group" "example_child" { 20 | name = "e2e-test-${random_string.example.result}" 21 | display_name = "${var.child_management_group_display_name} ${random_string.example.result}" 22 | parent_management_group_id = data.azurerm_management_group.example_parent.id 23 | } 24 | 25 | resource "azurerm_resource_group" "management" { 26 | provider = azurerm.management 27 | name = "e2e-test-management-azurerm-${random_string.example.result}" 28 | location = local.starter_location 29 | } 30 | 31 | resource "azurerm_resource_group" "connectivity" { 32 | provider = azurerm.connectivity 33 | name = "e2e-test-connectivity-azurerm-${random_string.example.result}" 34 | location = local.starter_location 35 | } 36 | 37 | resource "azurerm_resource_group" "identity" { 38 | provider = azurerm.identity 39 | name = "e2e-test-identity-azurerm-${random_string.example.result}" 40 | location = local.starter_location 41 | } 42 | 43 | resource "azurerm_resource_group" "security" { 44 | provider = azurerm.security 45 | name = "e2e-test-security-azurerm-${random_string.example.result}" 46 | location = local.starter_location 47 | } 48 | 49 | resource "azapi_resource" "resource_group_management" { 50 | parent_id = "/subscriptions/${var.subscription_ids["management"]}" 51 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 52 | name = "e2e-test-management-azapi-${random_string.example.result}" 53 | location = local.starter_location 54 | body = { 55 | properties = {} 56 | } 57 | schema_validation_enabled = false 58 | } 59 | 60 | resource "azapi_resource" "resource_group_connectivity" { 61 | parent_id = "/subscriptions/${var.subscription_ids["connectivity"]}" 62 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 63 | name = "e2e-test-connectivity-azapi-${random_string.example.result}" 64 | location = local.starter_location 65 | body = { 66 | properties = {} 67 | } 68 | schema_validation_enabled = false 69 | } 70 | 71 | resource "azapi_resource" "resource_group_identity" { 72 | parent_id = "/subscriptions/${var.subscription_ids["identity"]}" 73 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 74 | name = "e2e-test-identity-azapi-${random_string.example.result}" 75 | location = local.starter_location 76 | body = { 77 | properties = {} 78 | } 79 | schema_validation_enabled = false 80 | } 81 | 82 | resource "azapi_resource" "resource_group_security" { 83 | parent_id = "/subscriptions/${var.subscription_ids["security"]}" 84 | type = "Microsoft.Resources/resourceGroups@2021-04-01" 85 | name = "e2e-test-security-azapi-${random_string.example.result}" 86 | location = local.starter_location 87 | body = { 88 | properties = {} 89 | } 90 | schema_validation_enabled = false 91 | } 92 | 93 | module "subscription" { 94 | source = "../modules/test_module" 95 | subscription_id = data.azurerm_subscription.current.subscription_id 96 | } 97 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/config-templating/locals.config.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | starter_locations = { for i, location in var.starter_locations : "starter_location_${format("%02d", i + 1)}" => location } 3 | starter_locations_short = { 4 | for i, location in var.starter_locations : 5 | "starter_location_${format("%02d", i + 1)}_short" => coalesce( 6 | # 1. User override (if provided) 7 | try(var.starter_locations_short["starter_location_${format("%02d", i + 1)}_short"], null), 8 | # 2. Official geo_code from regions module 9 | try(module.regions.regions_by_name[location].geo_code, null), 10 | # 3. Calculated short_name from regions module 11 | try(module.regions.regions_by_name[location].short_name, null), 12 | # 4. Last resort: full region name 13 | location 14 | ) 15 | } 16 | built_in_replacements = merge( 17 | local.starter_locations, 18 | local.starter_locations_short, 19 | { 20 | root_parent_management_group_id = var.root_parent_management_group_id == "" ? data.azurerm_client_config.current.tenant_id : var.root_parent_management_group_id 21 | subscription_id_connectivity = var.subscription_id_connectivity 22 | subscription_id_identity = var.subscription_id_identity 23 | subscription_id_management = var.subscription_id_management 24 | subscription_id_security = var.subscription_id_security 25 | }) 26 | } 27 | 28 | # Custom name replacements 29 | locals { 30 | custom_names_json = replace(replace(tostring(jsonencode(var.custom_replacements.names)), "\"true\"", "{{string_true}}"), "\"false\"", "{{string_false}}") 31 | custom_names_json_templated = templatestring(local.custom_names_json, local.built_in_replacements) 32 | custom_names_json_final = replace(replace(replace(replace(local.custom_names_json_templated, "\"true\"", "true"), "\"false\"", "false"), "{{string_true}}", "\"true\""), "{{string_false}}", "\"false\"") 33 | custom_names = jsondecode(local.custom_names_json_final) 34 | } 35 | 36 | locals { 37 | custom_name_replacements = merge(local.built_in_replacements, local.custom_names) 38 | } 39 | 40 | # Custom resource group identifiers 41 | locals { 42 | custom_resource_group_identifiers_json = replace(replace(tostring(jsonencode(var.custom_replacements.resource_group_identifiers)), "\"true\"", "{{string_true}}"), "\"false\"", "{{string_false}}") 43 | custom_resource_group_identifiers_json_templated = templatestring(local.custom_resource_group_identifiers_json, local.custom_name_replacements) 44 | custom_resource_group_identifiers_json_final = replace(replace(replace(replace(local.custom_resource_group_identifiers_json_templated, "\"true\"", "true"), "\"false\"", "false"), "{{string_true}}", "\"true\""), "{{string_false}}", "\"false\"") 45 | custom_resource_group_identifiers = jsondecode(local.custom_resource_group_identifiers_json_final) 46 | } 47 | 48 | locals { 49 | custom_resource_group_replacements = merge(local.custom_name_replacements, local.custom_resource_group_identifiers) 50 | } 51 | 52 | # Custom resource identifiers 53 | locals { 54 | custom_resource_identifiers_json = replace(replace(tostring(jsonencode(var.custom_replacements.resource_identifiers)), "\"true\"", "{{string_true}}"), "\"false\"", "{{string_false}}") 55 | custom_resource_identifiers_json_templated = templatestring(local.custom_resource_identifiers_json, local.custom_resource_group_replacements) 56 | custom_resource_identifiers_json_final = replace(replace(replace(replace(local.custom_resource_identifiers_json_templated, "\"true\"", "true"), "\"false\"", "false"), "{{string_true}}", "\"true\""), "{{string_false}}", "\"false\"") 57 | custom_resource_identifiers = jsondecode(local.custom_resource_identifiers_json_final) 58 | } 59 | 60 | locals { 61 | final_replacements = merge(local.custom_resource_group_replacements, local.custom_resource_identifiers) 62 | } 63 | 64 | locals { 65 | outputs_json = { for key, value in var.inputs : key => replace(replace(tostring(jsonencode(value)), "\"true\"", "{{string_true}}"), "\"false\"", "{{string_false}}") } 66 | outputs_json_templated = { for key, value in local.outputs_json : key => templatestring(value, local.final_replacements) } 67 | outputs_json_final = { for key, value in local.outputs_json_templated : key => replace(replace(replace(replace(value, "\"true\"", "true"), "\"false\"", "false"), "{{string_true}}", "\"true\""), "{{string_false}}", "\"false\"") } 68 | outputs = { for key, value in local.outputs_json_final : key => jsondecode(value) } 69 | } 70 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/variables.tf: -------------------------------------------------------------------------------- 1 | variable "starter_locations" { 2 | type = list(string) 3 | description = "The default for Azure resources. (e.g 'uksouth')" 4 | validation { 5 | condition = length(var.starter_locations) > 0 6 | error_message = "You must provide at least one starter location region." 7 | } 8 | validation { 9 | condition = var.connectivity_type == "none" || ((length(var.virtual_hubs) <= length(var.starter_locations)) || (length(var.hub_virtual_networks) <= length(var.starter_locations))) 10 | error_message = "The number of regions supplied in `starter_locations` must match the number of regions specified for connectivity." 11 | } 12 | } 13 | 14 | variable "starter_locations_short" { 15 | type = map(string) 16 | default = {} 17 | description = < -------------------------------------------------------------------------------- /templates/platform_landing_zone/outputs.tf: -------------------------------------------------------------------------------- 1 | output "dns_server_ip_address" { 2 | value = local.connectivity_enabled ? (local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].dns_server_ip_addresses : module.virtual_wan[0].dns_server_ip_address) : null 3 | } 4 | 5 | output "hub_and_spoke_vnet_virtual_network_resource_ids" { 6 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].virtual_network_resource_ids : null 7 | } 8 | 9 | output "hub_and_spoke_vnet_virtual_network_resource_names" { 10 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].virtual_network_resource_names : null 11 | } 12 | 13 | output "hub_and_spoke_vnet_bastion_host_public_ip_address" { 14 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].bastion_host_public_ip_address : null 15 | } 16 | 17 | output "hub_and_spoke_vnet_bastion_host_resource_ids" { 18 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].bastion_host_resource_ids : null 19 | } 20 | 21 | output "hub_and_spoke_vnet_bastion_host_dns_names" { 22 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].bastion_host_dns_names : null 23 | } 24 | 25 | output "hub_and_spoke_vnet_firewall_resource_ids" { 26 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].firewall_resource_ids : null 27 | } 28 | 29 | output "hub_and_spoke_vnet_firewall_resource_names" { 30 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].firewall_resource_names : null 31 | } 32 | 33 | output "hub_and_spoke_vnet_firewall_private_ip_address" { 34 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].firewall_private_ip_addresses : null 35 | } 36 | 37 | output "hub_and_spoke_vnet_firewall_public_ip_addresses" { 38 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].firewall_public_ip_addresses : null 39 | } 40 | 41 | output "hub_and_spoke_vnet_firewall_policies" { 42 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].firewall_policies : null 43 | } 44 | 45 | output "hub_and_spoke_vnet_route_tables_firewall" { 46 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].route_tables_firewall : null 47 | } 48 | 49 | output "hub_and_spoke_vnet_route_tables_user_subnets" { 50 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0].route_tables_user_subnets : null 51 | } 52 | 53 | output "hub_and_spoke_vnet_full_output" { 54 | value = local.connectivity_hub_and_spoke_vnet_enabled ? module.hub_and_spoke_vnet[0] : null 55 | } 56 | 57 | output "virtual_wan_resource_id" { 58 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].resource_id : null 59 | } 60 | 61 | output "virtual_wan_name" { 62 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].name : null 63 | } 64 | 65 | output "virtual_wan_virtual_hub_resource_ids" { 66 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].virtual_hub_resource_ids : null 67 | } 68 | 69 | output "virtual_wan_virtual_hub_resource_names" { 70 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].virtual_hub_resource_names : null 71 | } 72 | 73 | output "virtual_wan_firewall_resource_ids" { 74 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].firewall_resource_ids : null 75 | } 76 | 77 | output "virtual_wan_firewall_resource_names" { 78 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].firewall_resource_names : null 79 | } 80 | 81 | output "virtual_wan_firewall_private_ip_address" { 82 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].firewall_private_ip_address : null 83 | } 84 | 85 | output "virtual_wan_firewall_public_ip_addresses" { 86 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].firewall_public_ip_addresses : null 87 | } 88 | 89 | output "virtual_wan_firewall_policy_ids" { 90 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].firewall_policy_resource_ids : null 91 | } 92 | 93 | output "virtual_wan_express_route_gateway_resource_ids" { 94 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].express_route_gateway_resource_ids : null 95 | } 96 | 97 | output "virtual_wan_bastion_host_public_ip_address" { 98 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].bastion_host_public_ip_address : null 99 | } 100 | 101 | output "virtual_wan_bastion_host_resource_ids" { 102 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].bastion_host_resource_ids : null 103 | } 104 | 105 | output "virtual_wan_bastion_host_dns_names" { 106 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].bastion_host_dns_names : null 107 | } 108 | 109 | output "virtual_wan_private_dns_resolver_resource_ids" { 110 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].private_dns_resolver_resource_ids : null 111 | } 112 | 113 | output "virtual_wan_private_dns_resolver_resources" { 114 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].private_dns_resolver_resources : null 115 | } 116 | 117 | output "virtual_wan_sidecar_virtual_network_resource_ids" { 118 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].sidecar_virtual_network_resource_ids : null 119 | } 120 | 121 | output "virtual_wan_sidecar_virtual_network_resources" { 122 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0].sidecar_virtual_network_resources : null 123 | } 124 | 125 | output "virtual_wan_full_output" { 126 | value = local.connectivity_virtual_wan_enabled ? module.virtual_wan[0] : null 127 | } 128 | 129 | output "templated_inputs" { 130 | value = module.config.outputs 131 | } 132 | -------------------------------------------------------------------------------- /.github/workflows/pr-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: PR Tests 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | types: ['opened', 'reopened', 'synchronize', 'labeled'] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | id-token: write 13 | contents: read 14 | 15 | jobs: 16 | define-matrix: 17 | name: Define Matrix 18 | runs-on: ubuntu-latest 19 | 20 | outputs: 21 | matrix: ${{ steps.matrix.outputs.matrix }} 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 26 | - name: Generate Matrix 27 | id: matrix 28 | run: | 29 | $matrix = .github/tests/scripts/generate-matrix.ps1 -runNumber "${{ github.run_number }}" -primaryTestMode "plan" 30 | $matrixJson = ConvertTo-Json $matrix -Depth 10 -Compress 31 | Write-Host (ConvertTo-Json $matrix -Depth 10) 32 | Write-Output "matrix=$matrixJson" >> $env:GITHUB_OUTPUT 33 | shell: pwsh 34 | 35 | e2e-test: 36 | needs: define-matrix 37 | name: "${{ matrix.name }} (${{ matrix.ShortName }})" 38 | environment: 'CSUTF' 39 | strategy: 40 | max-parallel: 20 41 | fail-fast: false 42 | matrix: 43 | include: ${{ fromJSON(needs.define-matrix.outputs.matrix) }} 44 | 45 | runs-on: ubuntu-latest 46 | steps: 47 | 48 | - name: Checkout 49 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 50 | 51 | - name: Setup Terraform 52 | uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd 53 | with: 54 | terraform_version: latest 55 | terraform_wrapper: false 56 | 57 | - name: Setup Module Inputs 58 | run: | 59 | $mode = "${{ matrix.mode }}" 60 | $subscriptionModulePath = ".github/tests/subscription_lookup" 61 | 62 | $subscriptionNameConnectivity = "${{ matrix.subscriptionNameConnectivity }}" 63 | $subscriptionNameManagement = "${{ matrix.subscriptionNameManagement }}" 64 | $subscriptionNameIdentity = "${{ matrix.subscriptionNameIdentity }}" 65 | $subscriptionNameSecurity = "${{ matrix.subscriptionNameSecurity }}" 66 | 67 | $tfvars = @{ 68 | subscription_display_name_connectivity = $subscriptionNameConnectivity 69 | subscription_display_name_management = $subscriptionNameManagement 70 | subscription_display_name_identity = $subscriptionNameIdentity 71 | subscription_display_name_security = $subscriptionNameSecurity 72 | } 73 | 74 | $tfvars | ConvertTo-Json -Depth 10 | Out-File -FilePath "$subscriptionModulePath/terraform.tfvars.json" -Encoding utf8 -Force 75 | 76 | $lastExistCodeFromTerraform = 1 77 | $attempt = 1 78 | $maxAttempts = 10 79 | while ($lastExistCodeFromTerraform -ne 0 -and $attempt -le $maxAttempts) { 80 | terraform -chdir="$subscriptionModulePath" init 81 | if($LASTEXITCODE -eq 0) { 82 | terraform -chdir="$subscriptionModulePath" apply -auto-approve -input=false 83 | } 84 | $lastExistCodeFromTerraform = $LASTEXITCODE 85 | Start-Sleep -Seconds 10 86 | $attempt++ 87 | } 88 | 89 | $subscriptionIdConnectivity = (terraform -chdir="$subscriptionModulePath" output -raw subscription_id_connectivity) 90 | $subscriptionIdManagement = (terraform -chdir="$subscriptionModulePath" output -raw subscription_id_management) 91 | $subscriptionIdIdentity = (terraform -chdir="$subscriptionModulePath" output -raw subscription_id_identity) 92 | $subscriptionIdSecurity = (terraform -chdir="$subscriptionModulePath" output -raw subscription_id_security) 93 | 94 | Write-Host "Subscription ID Connectivity: $subscriptionIdConnectivity" 95 | Write-Host "Subscription ID Management: $subscriptionIdManagement" 96 | Write-Host "Subscription ID Identity: $subscriptionIdIdentity" 97 | Write-Host "Subscription ID Security: $subscriptionIdSecurity" 98 | 99 | $sourceVarFilePath = "${{ matrix.configFilePath }}" 100 | $sourceVarFileContent = Get-Content -Path $sourceVarFilePath -Raw 101 | $sourceVarFileContent = $sourceVarFileContent.Replace("", "uksouth").Replace("", "ukwest") 102 | $sourceVarFileContent | Out-File -FilePath $sourceVarFilePath -Encoding utf8 -Force 103 | 104 | $tfvarsFile = @{ 105 | subscription_ids = @{ 106 | connectivity = $subscriptionIdConnectivity 107 | management = $subscriptionIdManagement 108 | identity = $subscriptionIdIdentity 109 | security = $subscriptionIdSecurity 110 | } 111 | root_parent_management_group_id = "${{ matrix.managementGroupId }}" 112 | } 113 | 114 | $tfvarsFile | ConvertTo-Json -Depth 10 | Out-File -FilePath "${{ matrix.rootModuleFolderPath }}/terraform.tfvars.json" -Encoding utf8 -Force 115 | Write-Debug $tfvarsFile 116 | 117 | if($mode -ne "apply") { 118 | exit 0 119 | } 120 | 121 | $subscriptions = @( 122 | @{ 123 | ID = $subscriptionIdConnectivity 124 | Name = $subscriptionNameConnectivity 125 | }, 126 | @{ 127 | ID = $subscriptionIdManagement 128 | Name = $subscriptionNameManagement 129 | }, 130 | @{ 131 | ID = $subscriptionIdIdentity 132 | Name = $subscriptionNameIdentity 133 | }, 134 | @{ 135 | ID = $subscriptionIdSecurity 136 | Name = $subscriptionNameSecurity 137 | } 138 | ) 139 | 140 | foreach ($subscription in $subscriptions) { 141 | Write-Host "Running an apply against the $($subscription.Name) subscription to ensure the resource providers are registered. This may take a few minutes the first time it is run." 142 | $env:ARM_SUBSCRIPTION_ID = $subscription.ID 143 | terraform -chdir="$subscriptionModulePath" apply -auto-approve -input=false 144 | } 145 | 146 | shell: pwsh 147 | env: 148 | ARM_TENANT_ID: ${{ vars.ARM_TENANT_ID }} 149 | ARM_SUBSCRIPTION_ID: ${{ vars.ARM_SUBSCRIPTION_ID }} 150 | ARM_CLIENT_ID: ${{ vars.ARM_CLIENT_ID }} 151 | ARM_USE_OIDC: true 152 | 153 | - name: Run Terraform 154 | run: | 155 | $mode = "${{ matrix.mode }}" 156 | $sourceVarFilePath = "${{ matrix.configFilePath }}" 157 | $rootModuleFolderPath = "${{ matrix.rootModuleFolderPath }}" 158 | $splitNumber = ${{ matrix.splitNumber }} 159 | $splitIncrement = ${{ matrix.splitIncrement }} 160 | $shortName = "${{ matrix.ShortName }}" 161 | $subscriptionName = "${{ matrix.subscriptionName }}" 162 | $managementGroupId = "${{ matrix.managementGroupId }}" 163 | 164 | Write-Host "Running on subscription: $subscriptionName, management group: $managementGroupId, split number: $splitNumber, mode: $mode" 165 | 166 | $success = .github/tests/scripts/terraform-run.ps1 ` 167 | -mode $mode ` 168 | -rootModuleFolderPath $rootModuleFolderPath ` 169 | -sourceVarFilePath $sourceVarFilePath ` 170 | -splitNumber $splitNumber ` 171 | -splitIncrement $splitIncrement ` 172 | -shortName $shortName ` 173 | -logFolder "./logs" 174 | 175 | if(-not $success) { 176 | Write-Host "Terraform run failed." 177 | exit 1 178 | } 179 | 180 | shell: pwsh 181 | env: 182 | ARM_TENANT_ID: ${{ vars.ARM_TENANT_ID }} 183 | ARM_SUBSCRIPTION_ID: ${{ vars.ARM_SUBSCRIPTION_ID }} 184 | ARM_CLIENT_ID: ${{ vars.ARM_CLIENT_ID }} 185 | ARM_USE_OIDC: true 186 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/management_resources/variables.tf: -------------------------------------------------------------------------------- 1 | variable "management_resource_settings" { 2 | type = object({ 3 | automation_account_name = optional(string) 4 | location = string 5 | log_analytics_workspace_name = optional(string) 6 | resource_group_name = optional(string) 7 | data_collection_rules = optional(object({ 8 | change_tracking = object({ 9 | enabled = optional(bool, true) 10 | name = string 11 | location = optional(string) 12 | tags = optional(map(string)) 13 | }) 14 | vm_insights = object({ 15 | enabled = optional(bool, true) 16 | name = string 17 | location = optional(string) 18 | tags = optional(map(string)) 19 | }) 20 | defender_sql = object({ 21 | enabled = optional(bool, true) 22 | name = string 23 | location = optional(string) 24 | tags = optional(map(string)) 25 | enable_collection_of_sql_queries_for_security_research = optional(bool, false) 26 | }) 27 | })) 28 | log_analytics_solution_plans = optional(list(object({ 29 | product = string 30 | publisher = optional(string) 31 | }))) 32 | log_analytics_workspace_allow_resource_only_permissions = optional(bool, true) 33 | log_analytics_workspace_cmk_for_query_forced = optional(bool) 34 | log_analytics_workspace_daily_quota_gb = optional(number) 35 | log_analytics_workspace_internet_ingestion_enabled = optional(bool, true) 36 | log_analytics_workspace_internet_query_enabled = optional(bool, true) 37 | log_analytics_workspace_local_authentication_enabled = optional(bool, true) 38 | log_analytics_workspace_reservation_capacity_in_gb_per_day = optional(number) 39 | log_analytics_workspace_retention_in_days = optional(number) 40 | log_analytics_workspace_sku = optional(string) 41 | sentinel_onboarding = optional(object({ 42 | name = optional(string) 43 | customer_managed_key_enabled = optional(bool) 44 | })) 45 | tags = optional(map(string)) 46 | timeouts = optional(object({ 47 | sentinel_onboarding = optional(object({ 48 | create = optional(string) 49 | delete = optional(string) 50 | update = optional(string) 51 | read = optional(string) 52 | })) 53 | data_collection_rule = optional(object({ 54 | create = optional(string) 55 | delete = optional(string) 56 | update = optional(string) 57 | read = optional(string) 58 | })) 59 | }), {}) 60 | user_assigned_managed_identities = optional(object({ 61 | ama = object({ 62 | enabled = optional(bool) 63 | name = string 64 | location = optional(string) 65 | tags = optional(map(string)) 66 | }) 67 | })) 68 | }) 69 | default = null 70 | description = <. 139 | If it is set to false, then no telemetry will be collected. 140 | DESCRIPTION 141 | nullable = false 142 | } 143 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/examples/management-only/management.tfvars: -------------------------------------------------------------------------------- 1 | /* 2 | --- Built-in Replacements --- 3 | This file contains built-in replacements to avoid repeating the same hard-coded values. 4 | Replacements are denoted by the dollar-dollar curly braces token (e.g. $${starter_location_01}). The following details each built-in replacements that you can use: 5 | `starter_location_01`: This the primary an Azure location sourced from the `starter_locations` variable. This can be used to set the location of resources. 6 | `starter_location_02` to `starter_location_##`: These are the secondary Azure locations sourced from the `starter_locations` variable. This can be used to set the location of resources. 7 | `starter_location_01_short`: Short code for the primary Azure location. Defaults to the region geo_code, or short_name if no geo_code is available. Can be overridden via the starter_locations_short variable. 8 | `starter_location_02_short` to `starter_location_##_short`: Short codes for the secondary Azure locations. Same behavior and override rules as starter_location_01_short. 9 | `root_parent_management_group_id`: This is the id of the management group that the ALZ hierarchy will be nested under. 10 | `subscription_id_identity`: The subscription ID of the subscription to deploy the identity resources to, sourced from the variable `subscription_ids`. 11 | `subscription_id_connectivity`: The subscription ID of the subscription to deploy the connectivity resources to, sourced from the variable `subscription_ids`. 12 | `subscription_id_management`: The subscription ID of the subscription to deploy the management resources to, sourced from the variable `subscription_ids`. 13 | `subscription_id_security`: The subscription ID of the subscription to deploy the security resources to, sourced from the variable `subscription_ids`. 14 | */ 15 | 16 | /* 17 | --- Starter Locations --- 18 | You can define the Azure regions to use throughout the configuration. 19 | The first location will be used as the primary location, the second as the secondary location, and so on. 20 | */ 21 | starter_locations = [""] 22 | 23 | /* 24 | --- Custom Replacements --- 25 | You can define custom replacements to use throughout the configuration. 26 | */ 27 | custom_replacements = { 28 | /* 29 | --- Custom Name Replacements --- 30 | You can define custom names and other strings to use throughout the configuration. 31 | You can only use the built in replacements in this section. 32 | NOTE: You cannot refer to another custom name in this variable. 33 | */ 34 | names = { 35 | # Defender email security contact 36 | defender_email_security_contact = "replace_me@replace_me.com" 37 | 38 | # Resource group names 39 | management_resource_group_name = "rg-management-$${starter_location_01}" 40 | asc_export_resource_group_name = "rg-asc-export-$${starter_location_01}" 41 | 42 | # Resource names 43 | log_analytics_workspace_name = "law-management-$${starter_location_01}" 44 | ama_user_assigned_managed_identity_name = "uami-management-ama-$${starter_location_01}" 45 | dcr_change_tracking_name = "dcr-change-tracking" 46 | dcr_defender_sql_name = "dcr-defender-sql" 47 | dcr_vm_insights_name = "dcr-vm-insights" 48 | } 49 | 50 | /* 51 | --- Custom Resource Group Identifier Replacements --- 52 | You can define custom resource group identifiers to use throughout the configuration. 53 | You can only use the templated variables and custom names in this section. 54 | NOTE: You cannot refer to another custom resource group identifier in this variable. 55 | */ 56 | resource_group_identifiers = { 57 | management_resource_group_id = "/subscriptions/$${subscription_id_management}/resourcegroups/$${management_resource_group_name}" 58 | } 59 | 60 | /* 61 | --- Custom Resource Identifier Replacements --- 62 | You can define custom resource identifiers to use throughout the configuration. 63 | You can only use the templated variables, custom names and customer resource group identifiers in this variable. 64 | NOTE: You cannot refer to another custom resource identifier in this variable. 65 | */ 66 | resource_identifiers = { 67 | ama_change_tracking_data_collection_rule_id = "$${management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/$${dcr_change_tracking_name}" 68 | ama_mdfc_sql_data_collection_rule_id = "$${management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/$${dcr_defender_sql_name}" 69 | ama_vm_insights_data_collection_rule_id = "$${management_resource_group_id}/providers/Microsoft.Insights/dataCollectionRules/$${dcr_vm_insights_name}" 70 | ama_user_assigned_managed_identity_id = "$${management_resource_group_id}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/$${ama_user_assigned_managed_identity_name}" 71 | log_analytics_workspace_id = "$${management_resource_group_id}/providers/Microsoft.OperationalInsights/workspaces/$${log_analytics_workspace_name}" 72 | } 73 | } 74 | 75 | enable_telemetry = true 76 | 77 | /* 78 | --- Tags --- 79 | This variable can be used to apply tags to all resources that support it. Some resources allow overriding these tags. 80 | */ 81 | tags = { 82 | deployed_by = "terraform" 83 | source = "Azure Landing Zones Accelerator" 84 | } 85 | 86 | /* 87 | --- Management Resources --- 88 | You can use this section to customize the management resources that will be deployed. 89 | */ 90 | management_resource_settings = { 91 | enabled = true 92 | location = "$${starter_location_01}" 93 | log_analytics_workspace_name = "$${log_analytics_workspace_name}" 94 | resource_group_name = "$${management_resource_group_name}" 95 | user_assigned_managed_identities = { 96 | ama = { 97 | name = "$${ama_user_assigned_managed_identity_name}" 98 | } 99 | } 100 | data_collection_rules = { 101 | change_tracking = { 102 | name = "$${dcr_change_tracking_name}" 103 | } 104 | defender_sql = { 105 | name = "$${dcr_defender_sql_name}" 106 | } 107 | vm_insights = { 108 | name = "$${dcr_vm_insights_name}" 109 | } 110 | } 111 | } 112 | 113 | /* 114 | --- Management Groups and Policy --- 115 | You can use this section to customize the management groups and policies that will be deployed. 116 | You can further configure management groups and policy by supplying a `lib` folder. This is detailed in the Accelerator documentation. 117 | */ 118 | management_group_settings = { 119 | enabled = true 120 | # This is the name of the architecture that will be used to deploy the management resources. 121 | # It refers to the alz_custom.alz_architecture_definition.yaml file in the lib folder. 122 | # Do not change this value unless you have created another architecture definition 123 | # with the name value specified below. 124 | architecture_name = "alz_custom" 125 | location = "$${starter_location_01}" 126 | parent_resource_id = "$${root_parent_management_group_id}" 127 | policy_default_values = { 128 | ama_change_tracking_data_collection_rule_id = "$${ama_change_tracking_data_collection_rule_id}" 129 | ama_mdfc_sql_data_collection_rule_id = "$${ama_mdfc_sql_data_collection_rule_id}" 130 | ama_vm_insights_data_collection_rule_id = "$${ama_vm_insights_data_collection_rule_id}" 131 | ama_user_assigned_managed_identity_id = "$${ama_user_assigned_managed_identity_id}" 132 | ama_user_assigned_managed_identity_name = "$${ama_user_assigned_managed_identity_name}" 133 | log_analytics_workspace_id = "$${log_analytics_workspace_id}" 134 | /* 135 | # Example of allowed locations for Sovereign Landing Zones policies 136 | allowed_locations = [ 137 | "$${starter_location_01}" 138 | ] 139 | */ 140 | } 141 | subscription_placement = { 142 | identity = { 143 | subscription_id = "$${subscription_id_identity}" 144 | management_group_name = "identity" 145 | } 146 | connectivity = { 147 | subscription_id = "$${subscription_id_connectivity}" 148 | management_group_name = "connectivity" 149 | } 150 | management = { 151 | subscription_id = "$${subscription_id_management}" 152 | management_group_name = "management" 153 | } 154 | security = { 155 | subscription_id = "$${subscription_id_security}" 156 | management_group_name = "security" 157 | } 158 | } 159 | policy_assignments_to_modify = { 160 | alz = { 161 | policy_assignments = { 162 | Deploy-MDFC-Config-H224 = { 163 | parameters = { 164 | ascExportResourceGroupName = "$${asc_export_resource_group_name}" 165 | ascExportResourceGroupLocation = "$${starter_location_01}" 166 | emailSecurityContact = "$${defender_email_security_contact}" 167 | enableAscForServers = "DeployIfNotExists" 168 | enableAscForServersVulnerabilityAssessments = "DeployIfNotExists" 169 | enableAscForSql = "DeployIfNotExists" 170 | enableAscForAppServices = "DeployIfNotExists" 171 | enableAscForStorage = "DeployIfNotExists" 172 | enableAscForContainers = "DeployIfNotExists" 173 | enableAscForKeyVault = "DeployIfNotExists" 174 | enableAscForSqlOnVm = "DeployIfNotExists" 175 | enableAscForArm = "DeployIfNotExists" 176 | enableAscForOssDb = "DeployIfNotExists" 177 | enableAscForCosmosDbs = "DeployIfNotExists" 178 | enableAscForCspm = "DeployIfNotExists" 179 | } 180 | } 181 | } 182 | } 183 | connectivity = { 184 | policy_assignments = { 185 | Enable-DDoS-VNET = { 186 | enforcement_mode = "DoNotEnforce" 187 | } 188 | } 189 | } 190 | landingzones = { 191 | policy_assignments = { 192 | Enable-DDoS-VNET = { 193 | enforcement_mode = "DoNotEnforce" 194 | } 195 | } 196 | } 197 | corp = { 198 | policy_assignments = { 199 | Deploy-Private-DNS-Zones = { 200 | enforcement_mode = "DoNotEnforce" 201 | } 202 | } 203 | } 204 | } 205 | /* 206 | # Example of how to add management group role assignments 207 | management_group_role_assignments = { 208 | root_owner_role_assignment = { 209 | management_group_name = "root" 210 | role_definition_id_or_name = "Owner" 211 | principal_id = "00000000-0000-0000-0000-000000000000" 212 | } 213 | } 214 | */ 215 | # role_assignment_name_use_random_uuid = false # Uncomment this for backwards compatibility with previous naming convention 216 | } 217 | 218 | connectivity_type = "none" 219 | -------------------------------------------------------------------------------- /.github/tests/scripts/terraform-run.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$mode = "apply", 3 | [string]$rootModuleFolderPath = "./templates/platform_landing_zone", 4 | [string]$sourceVarFilePath = "./templates/platform_landing_zone/examples/full-multi-region/hub-and-spoke-vnet.tfvars", 5 | [int]$splitNumber = 1, 6 | [int]$splitIncrement = 1, 7 | [string]$shortName = "blah", 8 | [string]$logFolder = "./logs" 9 | ) 10 | 11 | function Invoke-TerraformWithRetry { 12 | param( 13 | [hashtable[]]$commands, 14 | [string]$workingDirectory, 15 | [string]$outputLog = "output.log", 16 | [string]$errorLog = "error.log", 17 | [int]$maxRetries = 10, 18 | [int]$retryDelayIncremental = 10, 19 | [string[]]$retryOn = @("429 Too Many Requests"), 20 | [switch]$printOutput, 21 | [switch]$printOutputOnError 22 | ) 23 | 24 | $retryCount = 0 25 | $shouldRetry = $true 26 | 27 | while ($shouldRetry -and $retryCount -le $maxRetries) { 28 | $shouldRetry = $false 29 | 30 | foreach ($command in $commands) { 31 | $commandName = $command.Command 32 | $arguments = $command.Arguments 33 | 34 | $localLogPath = $outputLog 35 | if($command.OutputLog) { 36 | $localLogPath = $command.OutputLog 37 | } 38 | 39 | $commandArguments = @("-chdir=$workingDirectory", $commandName) + $arguments 40 | 41 | Write-Host "Running Terraform $commandName with arguments: $($commandArguments -join ' ')" 42 | $process = Start-Process ` 43 | -FilePath "terraform" ` 44 | -ArgumentList $commandArguments ` 45 | -RedirectStandardOutput $localLogPath ` 46 | -RedirectStandardError $errorLog ` 47 | -PassThru ` 48 | -NoNewWindow ` 49 | -Wait 50 | 51 | if ($process.ExitCode -ne 0) { 52 | Write-Host "Terraform $commandName failed with exit code $($process.ExitCode)." 53 | 54 | if($retryOn -contains "*") { 55 | $shouldRetry = $true 56 | } else { 57 | $errorOutput = Get-Content -Path $errorLog 58 | foreach($line in $errorOutput) { 59 | foreach($retryError in $retryOn) { 60 | if ($line -like "*$retryError*") { 61 | Write-Host "Retrying Terraform $commandName due to error: $line" 62 | $shouldRetry = $true 63 | } 64 | } 65 | } 66 | } 67 | 68 | if ($shouldRetry) { 69 | Write-Host "Retrying Terraform $commandName due to error:" 70 | Get-Content -Path $errorLog | Write-Host 71 | $retryCount++ 72 | break 73 | } else { 74 | Write-Host "Terraform $commandName failed with exit code $($process.ExitCode). Check the logs for details." 75 | if($printOutputOnError) { 76 | Write-Host "Output Log:" 77 | Get-Content -Path $localLogPath | Write-Host 78 | } 79 | Write-Host "Error Log:" 80 | Get-Content -Path $errorLog | Write-Host 81 | return $false 82 | } 83 | } else { 84 | if($printOutput) { 85 | Write-Host "Output Log:" 86 | Get-Content -Path $localLogPath | Write-Host 87 | } 88 | } 89 | } 90 | if ($shouldRetry) { 91 | Write-Host "Retrying Terraform commands (attempt $retryCount of $maxRetries)..." 92 | $retryDelay = $retryDelayIncremental * $retryCount 93 | Write-Host "Waiting for $retryDelay seconds before retrying..." 94 | Start-Sleep -Seconds $retryDelay 95 | } 96 | } 97 | return $true 98 | } 99 | 100 | $destinationVarFilePath = "$rootModuleFolderPath/test.auto.tfvars" 101 | 102 | $fileContent = Get-Content -Path $sourceVarFilePath 103 | 104 | if(-not (Test-Path -Path $logFolder)) { 105 | New-Item -ItemType Directory -Path $logFolder | Out-Null 106 | } 107 | 108 | Write-Host "Processing config file: $sourceVarFilePath" 109 | 110 | $booleanConfigs = @() 111 | foreach($line in $fileContent) { 112 | if($line -match "^.+_enabled\s+=\strue$" -and -not $line.TrimStart().StartsWith("#")) { 113 | Write-Host "Found boolean config: $($line)" 114 | $booleanConfigs += $line 115 | } 116 | } 117 | 118 | $combinations = @() 119 | 120 | $configSplits = @("secondary_", "primary_") 121 | 122 | foreach($configSplit in $configSplits) { 123 | $filteredConfigs = $booleanConfigs | Where-Object { $_ -notlike "*$configSplit*" } 124 | $filteredConfigsConfigLength = $filteredConfigs.Count 125 | if($filteredConfigsConfigLength -le 1) { 126 | Write-Host "No boolean configs found for split '$configSplit'. Skipping." 127 | continue 128 | } 129 | $filteredConfigsMaxCount = [Convert]::ToInt32(("1" * $filteredConfigsConfigLength), 2) 130 | 131 | for($i = $filteredConfigsMaxCount; $i -ge 0; $i--) { 132 | $binaryString = [Convert]::ToString($i, 2).PadLeft($filteredConfigsConfigLength, '0') 133 | $booleanSplit = $binaryString.ToCharArray() | ForEach-Object { $_ -eq '1' } 134 | $combination = [ordered]@{} 135 | foreach($config in $booleanConfigs) { 136 | $combination[$config] = $true 137 | } 138 | 139 | for($index = 0; $index -lt $booleanSplit.Count; $index++) { 140 | $configKey = $filteredConfigs[$index] 141 | $combination[$configKey] = $booleanSplit[$index] 142 | } 143 | 144 | $combinations += $combination 145 | } 146 | } 147 | 148 | if($combinations.Count -eq 0) { 149 | $combinations += @{} 150 | } 151 | 152 | Write-Host "Found $($combinations.Count) combinations for $sourceVarFilePath" 153 | 154 | $combinationNumber = 1 155 | $splitCombinationNumber = $splitNumber 156 | foreach ($combination in $combinations) { 157 | if($combinationNumber -ne $splitCombinationNumber) { 158 | $combinationNumber++ 159 | continue 160 | } 161 | 162 | $testContent = @() 163 | $updatedLines = @() 164 | 165 | foreach ($line in $fileContent) { 166 | $updatedLine = $line 167 | if($combination.Contains($line)) { 168 | $setting = $combination[$line].ToString().ToLower() 169 | $updatedLine = $line -replace "true", $setting 170 | $updatedLines += $updatedLine 171 | } 172 | if($mode -eq "apply" -and $updatedLine -like "*_resource_group_name*" -and $updatedLine -match "rg-") { 173 | $updatedLine = $updatedLine -replace "rg-", "rg-${shortName}-" 174 | } 175 | if($mode -eq "apply" -and $updatedLine -like "*management_group_name*") { 176 | $updatedLine = $updatedLine -replace " `"", " `"${shortName}-" 177 | } 178 | if($mode -eq "apply" -and $updatedLine -like "*alz = {*") { 179 | $updatedLine = $updatedLine -replace "alz = {", "${shortName}-alz = {" 180 | } 181 | $testContent += $updatedLine 182 | } 183 | 184 | $testContent | Out-File -FilePath $destinationVarFilePath -Encoding utf8 -Force 185 | 186 | Write-Host "Running $mode test for combination $combinationNumber of $($combinations.Count) with settings:" 187 | Write-Host "$($updatedLines | ConvertTo-Json -Depth 10)" 188 | 189 | $success = Invoke-TerraformWithRetry ` 190 | -commands @( 191 | @{ 192 | Command = "init" 193 | Arguments = @() 194 | OutputLog = "init.log" 195 | } 196 | ) ` 197 | -workingDirectory $rootModuleFolderPath ` 198 | -printOutputOnError 199 | 200 | if(-not $success) { 201 | Write-Host "Failed to initialize Terraform." 202 | Write-Host "Combination: $combinationNumber of $($combinations.Count)" 203 | Write-Host "$($updatedLines | ConvertTo-Json -Depth 10)" 204 | return $false 205 | } 206 | 207 | if($mode -eq "plan") { 208 | $success = Invoke-TerraformWithRetry ` 209 | -commands @(@{ 210 | Command = "plan" 211 | Arguments = @("-out=tfplan") 212 | }) ` 213 | -workingDirectory $rootModuleFolderPath ` 214 | -printOutputOnError 215 | 216 | if(-not $success) { 217 | Write-Host "Failed to generate plan." 218 | Write-Host "Combination: $combinationNumber of $($combinations.Count)" 219 | Write-Host "$($updatedLines | ConvertTo-Json -Depth 10)" 220 | return $false 221 | } 222 | 223 | $success = Invoke-TerraformWithRetry ` 224 | -commands @(@{ 225 | Command = "show" 226 | Arguments = @("-json", "tfplan") 227 | }) ` 228 | -workingDirectory $rootModuleFolderPath ` 229 | -outputLog "tfplan.json" 230 | 231 | if(-not $success) { 232 | Write-Host "Failed to generate plan JSON." 233 | Write-Host "Combination: $combinationNumber of $($combinations.Count)" 234 | Write-Host "$($updatedLines | ConvertTo-Json -Depth 10)" 235 | return $false 236 | } 237 | 238 | $planJson = Get-Content -Raw tfplan.json 239 | $planObject = ConvertFrom-Json $planJson -Depth 100 240 | 241 | $items = @{} 242 | foreach($change in $planObject.resource_changes) { 243 | $key = [System.String]::Join("-", $change.change.actions) 244 | if(!$items.ContainsKey($key)) { 245 | $items[$key] = 0 246 | } 247 | $items[$key]++ 248 | } 249 | 250 | Write-Host "Plan Summary" 251 | Write-Host (ConvertTo-Json $items -Depth 10) 252 | Write-Host "Terraform plan completed successfully for combination $combinationNumber of $($combinations.Count)." 253 | } 254 | 255 | if($mode -eq "apply") { 256 | $applySuccess = Invoke-TerraformWithRetry ` 257 | -commands @( 258 | @{ 259 | Command = "plan" 260 | Arguments = @("-out=tfplan") 261 | OutputLog = "$logFolder/apply-plan.log" 262 | }, 263 | @{ 264 | Command = "show" 265 | Arguments = @("-json", "tfplan") 266 | OutputLog = "$logFolder/apply-plan.json" 267 | } 268 | @{ 269 | Command = "apply" 270 | Arguments = @("tfplan") 271 | OutputLog = "$logFolder/apply.log" 272 | } 273 | ) ` 274 | -workingDirectory $rootModuleFolderPath 275 | 276 | if(-not $applySuccess) { 277 | Write-Host "Failed to apply Terraform." 278 | Write-Host "Combination: $combinationNumber of $($combinations.Count)" 279 | Write-Host "$($updatedLines | ConvertTo-Json -Depth 10)" 280 | } else { 281 | Write-Host "Terraform apply completed successfully for combination $combinationNumber of $($combinations.Count)." 282 | } 283 | 284 | $destroySuccess = Invoke-TerraformWithRetry ` 285 | -commands @( 286 | @{ 287 | Command = "plan" 288 | Arguments = @("-destroy","-out=tfplan") 289 | OutputLog = "$logFolder/destroy-plan.log" 290 | }, 291 | @{ 292 | Command = "apply" 293 | Arguments = @("tfplan") 294 | OutputLog = "$logFolder/destroy.log" 295 | } 296 | ) ` 297 | -workingDirectory $rootModuleFolderPath ` 298 | -retryOn @("*") 299 | 300 | if(-not $destroySuccess) { 301 | Write-Host "Failed to destroy Terraform resources." 302 | Write-Host "Combination: $combinationNumber of $($combinations.Count)" 303 | Write-Host "$($updatedLines | ConvertTo-Json -Depth 10)" 304 | } else { 305 | Write-Host "Terraform destroy completed successfully for combination $combinationNumber of $($combinations.Count)." 306 | } 307 | 308 | if(-not $applySuccess -or -not $destroySuccess) { 309 | Write-Host "Test failed for combination $combinationNumber of $($combinations.Count)." 310 | return $false 311 | } 312 | } 313 | 314 | $combinationNumber++ 315 | $splitCombinationNumber = $splitCombinationNumber + $splitIncrement 316 | Write-Debug "Next split combination number: $splitCombinationNumber" 317 | } 318 | 319 | return $true 320 | -------------------------------------------------------------------------------- /.github/workflows/end-to-end-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: End to End Tests 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | types: ['opened', 'reopened', 'synchronize', 'labeled'] 9 | workflow_dispatch: 10 | inputs: 11 | apply: 12 | description: 'Whether to run the apply and destroy steps' 13 | default: false 14 | type: boolean 15 | schedule: 16 | - cron: '0 3 * * 1-5' # Runs at 3:00 AM UTC on weekdays 17 | 18 | permissions: 19 | id-token: write 20 | contents: read 21 | 22 | jobs: 23 | define-matrix: 24 | name: Define Matrix 25 | runs-on: ubuntu-latest 26 | 27 | outputs: 28 | matrix: ${{ steps.matrix.outputs.matrix }} 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 33 | - name: Generate Matrix 34 | id: matrix 35 | run: | 36 | $matrix = .github/tests/scripts/generate-matrix.ps1 -runNumber "${{ github.run_number }}" -splitCount 15 -includeSecondaryTests 37 | $matrixJson = ConvertTo-Json $matrix -Depth 10 -Compress 38 | Write-Host (ConvertTo-Json $matrix -Depth 10) 39 | Write-Output "matrix=$matrixJson" >> $env:GITHUB_OUTPUT 40 | shell: pwsh 41 | 42 | e2e-test: 43 | needs: define-matrix 44 | name: "${{ matrix.name }} (${{ matrix.ShortName }})" 45 | environment: ${{ github.event_name == 'schedule' && 'CSUTFAUTO' || 'CSUTF' }} 46 | if: "${{ github.repository == 'Azure/alz-terraform-accelerator' && (contains(github.event.pull_request.labels.*.name, 'PR: Safe to test 🧪') || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') }}" 47 | strategy: 48 | max-parallel: 20 49 | fail-fast: false 50 | matrix: 51 | include: ${{ fromJSON(needs.define-matrix.outputs.matrix) }} 52 | 53 | runs-on: ubuntu-latest 54 | steps: 55 | 56 | - name: Checkout 57 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 58 | 59 | - name: Setup Terraform 60 | uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd 61 | with: 62 | terraform_version: latest 63 | terraform_wrapper: false 64 | 65 | - name: Setup Module Inputs 66 | run: | 67 | $mode = "${{ matrix.mode }}" 68 | $subscriptionModulePath = ".github/tests/subscription_lookup" 69 | 70 | $subscriptionNameConnectivity = "${{ matrix.subscriptionNameConnectivity }}" 71 | $subscriptionNameManagement = "${{ matrix.subscriptionNameManagement }}" 72 | $subscriptionNameIdentity = "${{ matrix.subscriptionNameIdentity }}" 73 | $subscriptionNameSecurity = "${{ matrix.subscriptionNameSecurity }}" 74 | 75 | $tfvars = @{ 76 | subscription_display_name_connectivity = $subscriptionNameConnectivity 77 | subscription_display_name_management = $subscriptionNameManagement 78 | subscription_display_name_identity = $subscriptionNameIdentity 79 | subscription_display_name_security = $subscriptionNameSecurity 80 | } 81 | 82 | $tfvars | ConvertTo-Json -Depth 10 | Out-File -FilePath "$subscriptionModulePath/terraform.tfvars.json" -Encoding utf8 -Force 83 | 84 | $lastExistCodeFromTerraform = 1 85 | $attempt = 1 86 | $maxAttempts = 10 87 | while ($lastExistCodeFromTerraform -ne 0 -and $attempt -le $maxAttempts) { 88 | terraform -chdir="$subscriptionModulePath" init 89 | if($LASTEXITCODE -eq 0) { 90 | terraform -chdir="$subscriptionModulePath" apply -auto-approve -input=false 91 | } 92 | $lastExistCodeFromTerraform = $LASTEXITCODE 93 | Start-Sleep -Seconds 10 94 | $attempt++ 95 | } 96 | 97 | $subscriptionIdConnectivity = (terraform -chdir="$subscriptionModulePath" output -raw subscription_id_connectivity) 98 | $subscriptionIdManagement = (terraform -chdir="$subscriptionModulePath" output -raw subscription_id_management) 99 | $subscriptionIdIdentity = (terraform -chdir="$subscriptionModulePath" output -raw subscription_id_identity) 100 | $subscriptionIdSecurity = (terraform -chdir="$subscriptionModulePath" output -raw subscription_id_security) 101 | 102 | Write-Host "Subscription ID Connectivity: $subscriptionIdConnectivity" 103 | Write-Host "Subscription ID Management: $subscriptionIdManagement" 104 | Write-Host "Subscription ID Identity: $subscriptionIdIdentity" 105 | Write-Host "Subscription ID Security: $subscriptionIdSecurity" 106 | 107 | $sourceVarFilePath = "${{ matrix.configFilePath }}" 108 | $sourceVarFileContent = Get-Content -Path $sourceVarFilePath -Raw 109 | $sourceVarFileContent = $sourceVarFileContent.Replace("", "uksouth").Replace("", "ukwest") 110 | $sourceVarFileContent | Out-File -FilePath $sourceVarFilePath -Encoding utf8 -Force 111 | 112 | $tfvarsFile = @{ 113 | subscription_ids = @{ 114 | connectivity = $subscriptionIdConnectivity 115 | management = $subscriptionIdManagement 116 | identity = $subscriptionIdIdentity 117 | security = $subscriptionIdSecurity 118 | } 119 | root_parent_management_group_id = "${{ matrix.managementGroupId }}" 120 | } 121 | 122 | $tfvarsFile | ConvertTo-Json -Depth 10 | Out-File -FilePath "${{ matrix.rootModuleFolderPath }}/terraform.tfvars.json" -Encoding utf8 -Force 123 | Write-Debug $tfvarsFile 124 | 125 | if($mode -ne "apply") { 126 | exit 0 127 | } 128 | 129 | $subscriptions = @( 130 | @{ 131 | ID = $subscriptionIdConnectivity 132 | Name = $subscriptionNameConnectivity 133 | }, 134 | @{ 135 | ID = $subscriptionIdManagement 136 | Name = $subscriptionNameManagement 137 | }, 138 | @{ 139 | ID = $subscriptionIdIdentity 140 | Name = $subscriptionNameIdentity 141 | }, 142 | @{ 143 | ID = $subscriptionIdSecurity 144 | Name = $subscriptionNameSecurity 145 | } 146 | ) 147 | 148 | foreach ($subscription in $subscriptions) { 149 | Write-Host "Running an apply against the $($subscription.Name) subscription to ensure the resource providers are registered. This may take a few minutes the first time it is run." 150 | $env:ARM_SUBSCRIPTION_ID = $subscription.ID 151 | terraform -chdir="$subscriptionModulePath" apply -auto-approve -input=false 152 | } 153 | 154 | shell: pwsh 155 | env: 156 | ARM_TENANT_ID: ${{ vars.ARM_TENANT_ID }} 157 | ARM_SUBSCRIPTION_ID: ${{ vars.ARM_SUBSCRIPTION_ID }} 158 | ARM_CLIENT_ID: ${{ vars.ARM_CLIENT_ID }} 159 | ARM_USE_OIDC: true 160 | 161 | - name: Azure login 162 | if: ${{ matrix.mode == 'apply' && always() }} 163 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 164 | with: 165 | client-id: ${{ vars.ARM_CLIENT_ID }} 166 | tenant-id: ${{ vars.ARM_TENANT_ID }} 167 | subscription-id: ${{ vars.ARM_SUBSCRIPTION_ID }} 168 | 169 | - name: Pre Test Clean Environment 170 | if: ${{ matrix.mode == 'apply' && always() }} 171 | run: | 172 | $environmentNumber = ${{ matrix.environmentNumber }} 173 | .github/tests/scripts/test-cleanup.ps1 ` 174 | -managementGroupStartNumber $environmentNumber ` 175 | -managementGroupCount 1 ` 176 | -subscriptionStartNumber $environmentNumber ` 177 | -subscriptionCount 1 178 | shell: pwsh 179 | 180 | - name: Setup Terraform Lib Folder 181 | if: ${{ matrix.mode == 'apply' }} 182 | run: | 183 | $sourceVarFilePath = "${{ matrix.configFilePath }}" 184 | $sourceVarFileFolderPath = Split-Path -Path $sourceVarFilePath -Parent 185 | $sourceLibFolderPath = "$sourceVarFileFolderPath/lib" 186 | 187 | if(Test-Path -Path $sourceLibFolderPath) { 188 | $targetLibFolderPath = "${{ matrix.rootModuleFolderPath }}/lib" 189 | Write-Host "Copying library files from $sourceLibFolderPath to $targetLibFolderPath" 190 | Copy-Item -Path "$sourceLibFolderPath/*" -Destination $targetLibFolderPath -Recurse -Force 191 | } 192 | 193 | $architectureFilePath = "${{ matrix.rootModuleFolderPath }}/lib/architecture_definitions/alz_custom.alz_architecture_definition.yaml" 194 | 195 | $shortName = "${{ matrix.ShortName }}" 196 | 197 | $content = Get-Content -Path $architectureFilePath 198 | $updatedContent = @() 199 | foreach ($line in $content) { 200 | $updatedLine = $line 201 | if ($line -like "*id: *" -and $line -notlike "*parent_id: null*") { 202 | $updatedLine = $line -replace "id: ", "id: $($shortName)-" 203 | } 204 | $updatedContent += $updatedLine 205 | } 206 | 207 | $updatedContent | Out-File -FilePath $architectureFilePath -Encoding utf8 -Force 208 | 209 | shell: pwsh 210 | 211 | - name: Run Terraform 212 | run: | 213 | $mode = "${{ matrix.mode }}" 214 | $sourceVarFilePath = "${{ matrix.configFilePath }}" 215 | $rootModuleFolderPath = "${{ matrix.rootModuleFolderPath }}" 216 | $splitNumber = ${{ matrix.splitNumber }} 217 | $splitIncrement = ${{ matrix.splitIncrement }} 218 | $shortName = "${{ matrix.ShortName }}" 219 | $subscriptionName = "${{ matrix.subscriptionName }}" 220 | $managementGroupId = "${{ matrix.managementGroupId }}" 221 | 222 | Write-Host "Running on subscription: $subscriptionName, management group: $managementGroupId, split number: $splitNumber, mode: $mode" 223 | 224 | $success = .github/tests/scripts/terraform-run.ps1 ` 225 | -mode $mode ` 226 | -rootModuleFolderPath $rootModuleFolderPath ` 227 | -sourceVarFilePath $sourceVarFilePath ` 228 | -splitNumber $splitNumber ` 229 | -splitIncrement $splitIncrement ` 230 | -shortName $shortName ` 231 | -logFolder "./logs" 232 | 233 | if(-not $success) { 234 | Write-Host "Terraform run failed." 235 | exit 1 236 | } 237 | 238 | shell: pwsh 239 | env: 240 | ARM_TENANT_ID: ${{ vars.ARM_TENANT_ID }} 241 | ARM_SUBSCRIPTION_ID: ${{ vars.ARM_SUBSCRIPTION_ID }} 242 | ARM_CLIENT_ID: ${{ vars.ARM_CLIENT_ID }} 243 | ARM_USE_OIDC: true 244 | 245 | - name: Post Test Clean Environment 246 | if: ${{ matrix.mode == 'apply' && always() }} 247 | run: | 248 | $environmentNumber = ${{ matrix.environmentNumber }} 249 | .github/tests/scripts/test-cleanup.ps1 ` 250 | -managementGroupStartNumber $environmentNumber ` 251 | -managementGroupCount 1 ` 252 | -subscriptionStartNumber $environmentNumber ` 253 | -subscriptionCount 1 254 | shell: pwsh 255 | 256 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 257 | if: ${{ always() && matrix.mode == 'apply' }} 258 | name: Upload logs 259 | with: 260 | name: ${{ matrix.name }}-logs 261 | path: ./logs 262 | -------------------------------------------------------------------------------- /templates/platform_landing_zone/modules/management_groups/variables.tf: -------------------------------------------------------------------------------- 1 | variable "management_group_settings" { 2 | type = object({ 3 | architecture_name = optional(string, "alz_custom") 4 | parent_resource_id = string 5 | location = string 6 | policy_default_values = optional(any) 7 | policy_assignments_to_modify = optional(any) 8 | management_group_hierarchy_settings = optional(object({ 9 | default_management_group_name = string 10 | require_authorization_for_group_creation = optional(bool, true) 11 | update_existing = optional(bool, false) 12 | })) 13 | partner_id = optional(string) 14 | retries = optional(object({ 15 | management_groups = optional(object({ 16 | error_message_regex = optional(list(string)) 17 | interval_seconds = optional(number) 18 | max_interval_seconds = optional(number) 19 | multiplier = optional(number) 20 | randomization_factor = optional(number) 21 | })) 22 | role_definitions = optional(object({ 23 | error_message_regex = optional(list(string)) 24 | interval_seconds = optional(number) 25 | max_interval_seconds = optional(number) 26 | multiplier = optional(number) 27 | randomization_factor = optional(number) 28 | })) 29 | role_assignments = optional(object({ 30 | error_message_regex = optional(list(string)) 31 | interval_seconds = optional(number) 32 | max_interval_seconds = optional(number) 33 | multiplier = optional(number) 34 | randomization_factor = optional(number) 35 | })) 36 | policy_definitions = optional(object({ 37 | error_message_regex = optional(list(string)) 38 | interval_seconds = optional(number) 39 | max_interval_seconds = optional(number) 40 | multiplier = optional(number) 41 | randomization_factor = optional(number) 42 | })) 43 | policy_set_definitions = optional(object({ 44 | error_message_regex = optional(list(string)) 45 | interval_seconds = optional(number) 46 | max_interval_seconds = optional(number) 47 | multiplier = optional(number) 48 | randomization_factor = optional(number) 49 | })) 50 | policy_assignments = optional(object({ 51 | error_message_regex = optional(list(string)) 52 | interval_seconds = optional(number) 53 | max_interval_seconds = optional(number) 54 | multiplier = optional(number) 55 | randomization_factor = optional(number) 56 | })) 57 | policy_role_assignments = optional(object({ 58 | error_message_regex = optional(list(string)) 59 | interval_seconds = optional(number) 60 | max_interval_seconds = optional(number) 61 | multiplier = optional(number) 62 | randomization_factor = optional(number) 63 | })) 64 | hierarchy_settings = optional(object({ 65 | error_message_regex = optional(list(string)) 66 | interval_seconds = optional(number) 67 | max_interval_seconds = optional(number) 68 | multiplier = optional(number) 69 | randomization_factor = optional(number) 70 | })) 71 | subscription_placement = optional(object({ 72 | error_message_regex = optional(list(string)) 73 | interval_seconds = optional(number) 74 | max_interval_seconds = optional(number) 75 | multiplier = optional(number) 76 | randomization_factor = optional(number) 77 | })) 78 | }), {}) 79 | subscription_placement = optional(map(object({ 80 | subscription_id = string 81 | management_group_name = string 82 | }))) 83 | timeouts = optional(object({ 84 | management_group = optional(object({ 85 | create = optional(string, "60m") 86 | delete = optional(string, "60m") 87 | update = optional(string, "60m") 88 | read = optional(string, "60m") 89 | }), {}) 90 | role_definition = optional(object({ 91 | create = optional(string, "60m") 92 | delete = optional(string, "60m") 93 | update = optional(string, "60m") 94 | read = optional(string, "60m") 95 | }), {}) 96 | role_assignment = optional(object({ 97 | create = optional(string, "60m") 98 | delete = optional(string, "60m") 99 | update = optional(string, "60m") 100 | read = optional(string, "60m") 101 | }), {}) 102 | policy_definition = optional(object({ 103 | create = optional(string, "60m") 104 | delete = optional(string, "60m") 105 | update = optional(string, "60m") 106 | read = optional(string, "60m") 107 | }), {}) 108 | policy_set_definition = optional(object({ 109 | create = optional(string, "60m") 110 | delete = optional(string, "60m") 111 | update = optional(string, "60m") 112 | read = optional(string, "60m") 113 | }), {}) 114 | policy_assignment = optional(object({ 115 | create = optional(string, "60m") 116 | delete = optional(string, "60m") 117 | update = optional(string, "60m") 118 | read = optional(string, "60m") 119 | }), {}) 120 | policy_role_assignment = optional(object({ 121 | create = optional(string, "60m") 122 | delete = optional(string, "60m") 123 | update = optional(string, "60m") 124 | read = optional(string, "60m") 125 | }), {}) 126 | }), {}) 127 | dependencies = optional(object({ 128 | policy_role_assignments = optional(any) 129 | policy_assignments = optional(any) 130 | })) 131 | override_policy_definition_parameter_assign_permissions_set = optional(set(object({ 132 | definition_name = string 133 | parameter_name = string 134 | }))) 135 | override_policy_definition_parameter_assign_permissions_unset = optional(set(object({ 136 | definition_name = string 137 | parameter_name = string 138 | }))) 139 | management_group_role_assignments = optional(map(object({ 140 | management_group_name = string 141 | role_definition_id_or_name = string 142 | principal_id = string 143 | description = optional(string) 144 | skip_service_principal_aad_check = optional(bool, false) 145 | condition = optional(string) 146 | condition_version = optional(string) 147 | delegated_managed_identity_resource_id = optional(string) 148 | principal_type = optional(string) 149 | }))) 150 | role_assignment_definition_lookup_enabled = optional(bool, true) 151 | policy_assignment_non_compliance_message_settings = optional(object({ 152 | fallback_message_enabled = optional(bool) 153 | fallback_message = optional(string) 154 | fallback_message_unsupported_assignments = optional(list(string)) 155 | enforcement_mode_placeholder = optional(string) 156 | enforced_replacement = optional(string) 157 | not_enforced_replacement = optional(string) 158 | })) 159 | role_assignment_name_use_random_uuid = optional(bool, true) 160 | }) 161 | default = null 162 | description = <. 252 | If it is set to false, then no telemetry will be collected. 253 | DESCRIPTION 254 | nullable = false 255 | } 256 | --------------------------------------------------------------------------------