├── .config └── ALZ-Powershell.config.json ├── .github ├── ISSUE_TEMPLATE │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── linters │ ├── .markdown-lint.yml │ └── .yaml-lint.yml ├── policies │ └── resourceManagement.yml ├── tests │ ├── README.md │ ├── cleanup-scripts │ │ ├── cleanup_github-repositories.ps1 │ │ └── cleanup_resouce_groups.ps1 │ └── scripts │ │ ├── azuredevops-pipeline-run.ps1 │ │ ├── destroy.ps1 │ │ ├── generate-matrix.ps1 │ │ └── github-action-run.ps1 └── workflows │ ├── docs-fmt-test.yml │ ├── end-to-end-test.yml │ ├── pr-title-check.yml │ ├── release.yml │ ├── scorecard.yml │ └── super-linter.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── alz ├── azuredevops │ ├── data.tf │ ├── locals.files.tf │ ├── locals.pipelines.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── pipelines │ │ ├── bicep │ │ │ ├── main │ │ │ │ ├── cd.yaml │ │ │ │ └── ci.yaml │ │ │ └── templates │ │ │ │ ├── cd-template.yaml │ │ │ │ ├── ci-template.yaml │ │ │ │ └── helpers │ │ │ │ ├── bicep-deploy.yaml │ │ │ │ ├── bicep-destroy.yaml │ │ │ │ ├── bicep-installer.yaml │ │ │ │ ├── bicep-on-demand-folder.yaml │ │ │ │ ├── bicep-templates.yaml │ │ │ │ └── bicep-variables.yaml │ │ └── terraform │ │ │ ├── main │ │ │ ├── cd.yaml │ │ │ └── ci.yaml │ │ │ └── templates │ │ │ ├── cd-template.yaml │ │ │ ├── ci-template.yaml │ │ │ └── helpers │ │ │ ├── terraform-apply.yaml │ │ │ ├── terraform-init.yaml │ │ │ ├── terraform-installer.yaml │ │ │ └── terraform-plan.yaml │ ├── terraform.tf │ └── variables.tf ├── github │ ├── actions │ │ ├── bicep │ │ │ ├── main │ │ │ │ └── workflows │ │ │ │ │ ├── cd.yaml │ │ │ │ │ └── ci.yaml │ │ │ └── templates │ │ │ │ ├── actions │ │ │ │ ├── bicep-deploy │ │ │ │ │ └── action.yaml │ │ │ │ ├── bicep-destroy │ │ │ │ │ └── action.yaml │ │ │ │ ├── bicep-first-deployment-check │ │ │ │ │ └── action.yaml │ │ │ │ ├── bicep-installer │ │ │ │ │ └── action.yaml │ │ │ │ ├── bicep-on-demand-folder │ │ │ │ │ └── action.yaml │ │ │ │ └── bicep-variables │ │ │ │ │ └── action.yaml │ │ │ │ └── workflows │ │ │ │ ├── cd-template.yaml │ │ │ │ └── ci-template.yaml │ │ └── terraform │ │ │ ├── main │ │ │ └── workflows │ │ │ │ ├── cd.yaml │ │ │ │ └── ci.yaml │ │ │ └── templates │ │ │ └── workflows │ │ │ ├── cd-template.yaml │ │ │ └── ci-template.yaml │ ├── data.tf │ ├── locals.files.tf │ ├── locals.tf │ ├── locals.workflows.tf │ ├── main.tf │ ├── outputs.tf │ ├── terraform.tf │ └── variables.tf └── local │ ├── data.tf │ ├── locals.files.tf │ ├── locals.terraform.script.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── scripts │ ├── bicep-deploy.ps1 │ ├── bicep-first-deployment-check.ps1 │ ├── bicep-get-variables.ps1 │ ├── bicep-on-demand-folder.ps1 │ └── deploy-local.ps1 │ ├── templates │ └── terraform-deploy-local.ps1 │ ├── terraform.tf │ └── variables.tf └── modules ├── azure ├── container_instances.tf ├── container_registry.tf ├── data.tf ├── locals.tf ├── managed_identity.tf ├── networking.tf ├── outputs.tf ├── private_endpoints.tf ├── resource_groups.tf ├── resource_providers.tf ├── role_assignments.tf ├── role_definitions.tf ├── storage.tf ├── terraform.tf └── variables.tf ├── azure_devops ├── agent_pool.tf ├── environment.tf ├── groups.tf ├── locals.tf ├── locals_pipelines.tf ├── outputs.tf ├── pipeline.tf ├── project.tf ├── repository_module.tf ├── repository_templates.tf ├── service_connections.tf ├── terraform.tf ├── variable_group.tf └── variables.tf ├── files ├── main.tf ├── outputs.tf └── variables.tf ├── github ├── action_variables.tf ├── data.tf ├── environment.tf ├── locals.tf ├── oidc_templates.tf ├── outputs.tf ├── repository_module.tf ├── repository_templates.tf ├── runner_group.tf ├── team.tf ├── terraform.tf └── variables.tf ├── resource_names ├── locals.tf ├── outputs.tf ├── providers.tf └── variables.tf └── template_architecture_definition ├── data.tf ├── locals.tf ├── outputs.tf ├── templates └── architecture_definition.json.tftpl ├── terraform.tf └── variables.tf /.config/ALZ-Powershell.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap_modules": { 3 | "alz_azuredevops": { 4 | "location": "alz/azuredevops", 5 | "short_name": "Azure DevOps: Azure Landing Zones", 6 | "description": "Azure Landing Zones with Azure DevOps", 7 | "starter_modules": "alz" 8 | }, 9 | "alz_github": { 10 | "location": "alz/github", 11 | "short_name": "GitHub: Azure Landing Zones", 12 | "description": "Azure Landing Zones with GitHub", 13 | "starter_modules": "alz" 14 | }, 15 | "alz_local": { 16 | "location": "alz/local", 17 | "short_name": "Local: Azure Landing Zones", 18 | "description": "Azure Landing Zones for local file system", 19 | "starter_modules": "alz" 20 | } 21 | }, 22 | "starter_modules": { 23 | "alz": { 24 | "terraform": { 25 | "url": "https://github.com/Azure/alz-terraform-accelerator", 26 | "release_artifact_name": "starter_modules.zip", 27 | "release_artifact_root_path": ".", 28 | "release_artifact_config_file": ".config/ALZ-Powershell.config.json" 29 | 30 | }, 31 | "bicep": { 32 | "url": "https://github.com/Azure/ALZ-Bicep", 33 | "release_artifact_name": "accelerator.zip", 34 | "release_artifact_root_path": ".", 35 | "release_artifact_config_file": "accelerator/.config/ALZ-Powershell-Auto.config.json" 36 | } 37 | } 38 | }, 39 | "validators": { 40 | "auth_scheme": { 41 | "Type": "AllowedValues", 42 | "Description": "A valid authentication scheme e.g. 'WorkloadIdentityFederation'", 43 | "AllowedValues": { 44 | "Display": true, 45 | "Values": [ 46 | "WorkloadIdentityFederation", 47 | "ManagedServiceIdentity" 48 | ] 49 | } 50 | }, 51 | "azure_subscription_id": { 52 | "Type": "Valid", 53 | "Description": "A valid subscription id GUID e.g. '12345678-1234-1234-1234-123456789012'", 54 | "Valid": "^( {){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1}$" 55 | }, 56 | "azure_name": { 57 | "Type": "Valid", 58 | "Description": "A valid Azure name e.g. 'my-azure-name'", 59 | "Valid": "^[a-zA-Z0-9]{2,10}(-[a-zA-Z0-9]{2,10}){0,1}(-[a-zA-Z0-9]{2,10})?$" 60 | }, 61 | "azure_name_section": { 62 | "Type": "Valid", 63 | "Description": "A valid Azure name with no hyphens and limited length e.g. 'abcd'", 64 | "Valid": "^[a-zA-Z0-9]{2,10}$" 65 | }, 66 | "guid": { 67 | "Type": "Valid", 68 | "Description": "A valid GUID e.g. '12345678-1234-1234-1234-123456789012'", 69 | "Valid": "^( {){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1}$" 70 | }, 71 | "cidr_range": { 72 | "Type": "Valid", 73 | "Description": "A valid CIDR range e.g '10.0.0.0/16'", 74 | "Valid": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(/(3[0-2]|[1-2][0-9]|[0-9]))$" 75 | }, 76 | "configuration_file_path": { 77 | "Type": "Valid", 78 | "Description": "A valid yaml or json configuration file path e.g. './my-folder/my-config-file.yaml' or `c:\\my-folder\\my-config-file.yaml`", 79 | "Valid": "^.+\\.(yaml|yml|json)$" 80 | }, 81 | "network_type": { 82 | "Type": "AllowedValues", 83 | "Description": "Networking Type'", 84 | "AllowedValues": { 85 | "Display": true, 86 | "Values": [ 87 | "hubNetworking", 88 | "hubNetworkingMultiRegion", 89 | "vwanConnectivity", 90 | "vwanConnectivityMultiRegion", 91 | "none" 92 | ] 93 | } 94 | }, 95 | "email": { 96 | "Type": "Valid", 97 | "Description": "A valid email address", 98 | "Valid": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" 99 | }, 100 | "azure_location": { 101 | "Type": "AllowedValues", 102 | "Description": "An Azure deployment location e.g. 'uksouth'", 103 | "AllowedValues": { 104 | "Display": false, 105 | "Values": [ "This is dynamically populated from Azure" ] 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # To get started with Dependabot version updates, you'll need to specify which 3 | # package ecosystems to update and where the package manifests are located. 4 | # Please see the documentation for all configuration options: 5 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "terraform" # See documentation for possible values 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/linters/.yaml-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | ignore: | 5 | **/bicep/**/ci-template.yaml 6 | **/bicep/**/cd-template.yaml 7 | *bicep-templates.yaml 8 | **/bicep/**/cd.yaml 9 | 10 | rules: 11 | # 500 chars should be enough, but don't fail if a line is longer 12 | line-length: 13 | max: 500 14 | level: warning 15 | truthy: 16 | check-keys: false 17 | level: warning 18 | -------------------------------------------------------------------------------- /.github/tests/README.md: -------------------------------------------------------------------------------- 1 | # End to End Tests 2 | 3 | ## Overview 4 | 5 | The end to end tests can be found in the ./github/workflows/end-to-end-test.yml action. 6 | 7 | - The tests are triggered by `workflow_dispatch` or on `pull_request` only when the label `PR: Safe to test 🧪` has been added to the PR. 8 | - The tests run against the environment `CSUTF`, which requires a manual approval to deploy. 9 | - The tests run as a matrix, targeting different OS, VCS, Terraform and Auth Methods. 10 | 11 | ## Test Process 12 | 13 | The test follow this process: 14 | 15 | 1. Check out the module from the PR merge branch. 16 | 1. Generate an `inputs.json` file that is used to override the prompts in the `ALZ` PowerShell module. 17 | 1. Install the `ALZ` PowerShell module. 18 | 1. Get the latest version tag for the live accelerator module. 19 | 1. Copy the `boostrap` and `template` folders into a folder named by the latest version tag. 20 | 1. Run the `New-New-ALZEnvironment` function to deploy the terraform. 21 | 1. Run a `terraform destroy` to clean up the environment. 22 | 23 | ## Environment 24 | 25 | The tests use a set of environemnts to managed by the ALZ team. These are: 26 | 27 | - Azure: 28 | - Tenant: CSU TF 29 | - Subscription: csu-tf-devops 30 | - User Assigned Managed Identity: alz-terraform-accelerator-cd-tests-identity (this has federated credentials) 31 | - Azure DevOps 32 | - Organisation: microsoft-azure-landing-zones-cd-tests 33 | - GiHub 34 | - Organisation: microsoft-azure-landingzones-cd-tests 35 | -------------------------------------------------------------------------------- /.github/tests/cleanup-scripts/cleanup_github-repositories.ps1: -------------------------------------------------------------------------------- 1 | # This file can be used to clean up GitHub repositories if there has been an issue with the End to End tests. 2 | # CAUTION: Make sure you are connected to the correct organization before running this script! 3 | $repos = gh repo list microsoft-azure-landing-zones-cd-tests --json name,owner | ConvertFrom-Json 4 | 5 | $repos | ForEach-Object -Parallel { 6 | $match = "*229*" 7 | $repoName = "$($_.owner.login)/$($_.name)" 8 | 9 | if($repoName -like $match) 10 | { 11 | Write-Host "Deleting repo: $repoName" 12 | gh repo delete $repoName --yes 13 | 14 | } 15 | } -ThrottleLimit 10 16 | -------------------------------------------------------------------------------- /.github/tests/cleanup-scripts/cleanup_resouce_groups.ps1: -------------------------------------------------------------------------------- 1 | # This file can be used to clean up Resource Groups if there has been an issue with the End to End tests. 2 | # CAUTION: Make sure you are connected to the correct subscription before running this script! 3 | az account show 4 | $resourceGroups = az group list --query "[?contains(name, '355-')]" | ConvertFrom-Json 5 | 6 | $resourceGroups | ForEach-Object -Parallel { 7 | Write-Host "Deleting resource group: $($_.name)" 8 | az group delete --name $_.name --yes 9 | } -ThrottleLimit 10 10 | -------------------------------------------------------------------------------- /.github/tests/scripts/destroy.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [int]$maximumRetries = 10, 3 | [int]$retryCount = 0, 4 | [int]$retryDelay = 10000, 5 | [string]$versionControlSystem 6 | ) 7 | 8 | $bootstrapDirectoryPath = "$($env:TARGET_FOLDER)/bootstrap/local/alz/$versionControlSystem" 9 | Write-Host "Bootstrap Directory Path: $bootstrapDirectoryPath" 10 | 11 | if(Test-Path -Path "$bootstrapDirectoryPath/terraform.tfvars.json") { 12 | Write-Host "Bootstrap tfvars Exists" 13 | } else { 14 | Write-Host "Bootstrap tfvars does not exist, so there is nothing to clean up. Exiting now." 15 | exit 0 16 | } 17 | 18 | $success = $false 19 | 20 | do { 21 | $retryCount++ 22 | try { 23 | 24 | Write-Host "Running Terraform Destroy" 25 | $starterModuleOverrideFolderPath = $env:STARTER_MODULE_FOLDER 26 | if($infrastructureAsCode -eq "terraform") { 27 | $starterModuleOverrideFolderPath = "$starterModuleOverrideFolderPath/templates" 28 | } 29 | Deploy-Accelerator -output "$($env:TARGET_FOLDER)" -inputs "./inputs.json" -bootstrapModuleOverrideFolderPath "$($env:BOOTSTRAP_MODULE_FOLDER)" -starterModuleOverrideFolderPath $starterModuleOverrideFolderPath -starterRelease "$($env.ALZ_ON_DEMAND_FOLDER_RELEASE_TAG)" -autoApprove -skipAlzModuleVersionRequirementsCheck -destroy -ErrorAction Stop 30 | if ($LastExitCode -eq 0) { 31 | $success = $true 32 | } else { 33 | throw "Failed to destroy the bootstrap environment." 34 | } 35 | } catch { 36 | Write-Host "Failed to destroy the bootstrap environment." 37 | Start-Sleep -Milliseconds $retryDelay 38 | } 39 | } while ($success -eq $false -and $retryCount -lt $maximumRetries) 40 | 41 | if ($success -eq $false) { 42 | throw "Failed to destroy the bootstrap environment after $maximumRetries attempts." 43 | } -------------------------------------------------------------------------------- /.github/tests/scripts/generate-matrix.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$runNumber = "999" 3 | ) 4 | 5 | $combinations = [ordered]@{ 6 | azuredevops_bicep = [ordered]@{ 7 | versionControlSystem = @("azuredevops") 8 | infrastructureAsCode = @("bicep") 9 | agentType = @("public", "private", "none") 10 | operatingSystem = @("ubuntu") 11 | starterModule = @("test") 12 | regions = @("multi") 13 | terraformVersion = @("latest") 14 | deployAzureResources = @("true") 15 | } 16 | github_bicep = [ordered]@{ 17 | versionControlSystem = @("github") 18 | infrastructureAsCode = @("bicep") 19 | agentType = @("public", "private", "none") 20 | operatingSystem = @("ubuntu") 21 | starterModule = @("test") 22 | regions = @("multi") 23 | terraformVersion = @("latest") 24 | deployAzureResources = @("true") 25 | } 26 | azuredevops_terraform = [ordered]@{ 27 | versionControlSystem = @("azuredevops") 28 | infrastructureAsCode = @("terraform") 29 | agentType = @("public", "private", "none") 30 | operatingSystem = @("ubuntu") 31 | starterModule = @("test_nested") 32 | regions = @("multi") 33 | terraformVersion = @("latest") 34 | deployAzureResources = @("true") 35 | } 36 | github_terraform = [ordered]@{ 37 | versionControlSystem = @("github") 38 | infrastructureAsCode = @("terraform") 39 | agentType = @("public", "private", "none") 40 | operatingSystem = @("ubuntu") 41 | starterModule = @("test_nested") 42 | regions = @("multi") 43 | terraformVersion = @("latest") 44 | deployAzureResources = @("true") 45 | } 46 | local_deploy_azure_resources_tests = [ordered]@{ 47 | versionControlSystem = @("local") 48 | infrastructureAsCode = @("terraform") 49 | agentType = @("none") 50 | operatingSystem = @("ubuntu", "windows", "macos") 51 | starterModule = @("test") 52 | regions = @("multi") 53 | terraformVersion = @("latest") 54 | deployAzureResources = @("true") 55 | } 56 | local_cross_os_terraform_version_tests = [ordered]@{ 57 | versionControlSystem = @("local") 58 | infrastructureAsCode = @("terraform") 59 | agentType = @("none") 60 | operatingSystem = @("ubuntu", "windows", "macos") 61 | starterModule = @("test") 62 | regions = @("multi") 63 | terraformVersion = @("1.5.0") 64 | deployAzureResources = @("false") 65 | } 66 | local_single_region_tests = [ordered]@{ 67 | versionControlSystem = @("local") 68 | infrastructureAsCode = @("terraform") 69 | agentType = @("none") 70 | operatingSystem = @("ubuntu") 71 | starterModule = @("test") 72 | regions = @("single") 73 | terraformVersion = @("latest") 74 | deployAzureResources = @("false") 75 | } 76 | local_starter_module_terraform_tests = [ordered]@{ 77 | versionControlSystem = @("local") 78 | infrastructureAsCode = @("terraform") 79 | agentType = @("none") 80 | operatingSystem = @("ubuntu") 81 | starterModule = @("platform_landing_zone", "sovereign_landing_zone", "financial_services_landing_zone") 82 | regions = @("multi") 83 | terraformVersion = @("latest") 84 | deployAzureResources = @("false") 85 | } 86 | local_starter_module_bicep_tests = [ordered]@{ 87 | versionControlSystem = @("local") 88 | infrastructureAsCode = @("bicep") 89 | agentType = @("none") 90 | operatingSystem = @("ubuntu") 91 | starterModule = @("complete") 92 | regions = @("multi") 93 | terraformVersion = @("latest") 94 | deployAzureResources = @("false") 95 | } 96 | } 97 | 98 | function Get-Hash([string]$textToHash) { 99 | $hasher = new-object System.Security.Cryptography.MD5CryptoServiceProvider 100 | $toHash = [System.Text.Encoding]::UTF8.GetBytes($textToHash) 101 | $hashByteArray = $hasher.ComputeHash($toHash) 102 | foreach($byte in $hashByteArray) 103 | { 104 | $result += "{0:X2}" -f $byte 105 | } 106 | return $result; 107 | } 108 | 109 | function Get-MatrixRecursively { 110 | param( 111 | $calculatedCombinations = @(), 112 | $indexes = [ordered]@{}, 113 | $definition, 114 | $runNumber 115 | ) 116 | 117 | if($indexes.Count -eq 0) { 118 | foreach($key in $definition.Keys) { 119 | $indexes.Add($key, @{ 120 | current = 0 121 | max = $definition[$key].Length - 1 122 | }) 123 | } 124 | } 125 | 126 | $combination = [ordered]@{} 127 | 128 | $name = "" 129 | 130 | foreach($key in $indexes.Keys) { 131 | $combinationValue = $definition[$key][$indexes[$key].current] 132 | $combination[$key] = $combinationValue 133 | $name = "$name-$combinationValue" 134 | } 135 | 136 | $combination.Name = $name.Trim("-") 137 | $combination.Hash = Get-Hash $name 138 | $combination.ShortName = "r" + $combination.Hash.Substring(0,5).ToLower() + "r" + $runNumber 139 | 140 | $calculatedCombinations += $combination 141 | 142 | $hasMore = $false 143 | foreach($key in $indexes.Keys) { 144 | if($indexes[$key].current -lt $indexes[$key].max) { 145 | $indexes[$key].current++ 146 | $hasMore = $true 147 | break 148 | } 149 | } 150 | 151 | if($hasMore) { 152 | $calculatedCombinations = Get-MatrixRecursively -calculatedCombinations $calculatedCombinations -indexes $indexes -definition $definition -runNumber $runNumber 153 | } 154 | 155 | return $calculatedCombinations 156 | } 157 | 158 | $finalMatrix = @() 159 | 160 | foreach($key in $combinations.Keys) { 161 | $finalMatrix += Get-MatrixRecursively -definition $combinations[$key] -runNumber $runNumber 162 | } 163 | 164 | return $finalMatrix 165 | -------------------------------------------------------------------------------- /.github/tests/scripts/github-action-run.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$organizationName, 3 | [string]$repositoryName, 4 | [string]$personalAccessToken, 5 | [string]$workflowFileName = "cd.yaml", 6 | [switch]$skipDestroy = $false, 7 | [int]$maximumRetries = 10, 8 | [int]$retryCount = 0, 9 | [int]$retryDelay = 10000, 10 | [string]$iac 11 | ) 12 | 13 | function Invoke-Workflow { 14 | param ( 15 | [string]$organizationName, 16 | [string]$repositoryName, 17 | [string]$workflowId, 18 | [string]$workflowAction = "", 19 | [string]$iac, 20 | [hashtable]$headers 21 | ) 22 | $workflowDispatchUrl = "https://api.github.com/repos/$organizationName/$repositoryName/actions/workflows/$workflowId/dispatches" 23 | Write-Host "Workflow Dispatch URL: $workflowDispatchUrl" 24 | 25 | $workflowDispatchBody = @{} 26 | if($workflowAction -eq "") { 27 | $workflowDispatchBody = @{ 28 | ref = "main" 29 | } | ConvertTo-Json -Depth 100 30 | } else { 31 | if($iac -eq "terraform") { 32 | $workflowDispatchBody = @{ 33 | ref = "main" 34 | inputs = @{ 35 | terraform_action = $workflowAction 36 | } 37 | } | ConvertTo-Json -Depth 100 38 | } 39 | 40 | if($iac -eq "bicep") { 41 | $workflowDispatchBody = @{ 42 | ref = "main" 43 | inputs = @{ 44 | destroy = ($workflowAction -eq "destroy").ToString().ToLower() 45 | } 46 | } | ConvertTo-Json -Depth 100 47 | } 48 | } 49 | 50 | $result = Invoke-RestMethod -Method POST -Uri $workflowDispatchUrl -Headers $headers -Body $workflowDispatchBody -StatusCodeVariable statusCode 51 | if ($statusCode -ne 204) { 52 | throw "Failed to dispatch the workflow. $result" 53 | } 54 | } 55 | 56 | function Wait-ForWorkflowRunToComplete { 57 | param ( 58 | [string]$organizationName, 59 | [string]$repositoryName, 60 | [hashtable]$headers 61 | ) 62 | 63 | $workflowRunUrl = "https://api.github.com/repos/$organizationName/$repositoryName/actions/runs" 64 | Write-Host "Workflow Run URL: $workflowRunUrl" 65 | 66 | $workflowRun = $null 67 | $workflowRunStatus = "" 68 | $workflowRunConclusion = "" 69 | while($workflowRunStatus -ne "completed") { 70 | Start-Sleep -Seconds 10 71 | 72 | $workflowRun = Invoke-RestMethod -Method GET -Uri $workflowRunUrl -Headers $headers -StatusCodeVariable statusCode 73 | if ($statusCode -lt 300) { 74 | $workflowRunStatus = $workflowRun.workflow_runs[0].status 75 | $workflowRunConclusion = $workflowRun.workflow_runs[0].conclusion 76 | Write-Host "Workflow Run Status: $workflowRunStatus - Conclusion: $workflowRunConclusion" 77 | } else { 78 | Write-Host "Failed to find the workflow run. Status Code: $statusCode" 79 | throw "Failed to find the workflow run." 80 | } 81 | } 82 | 83 | if($workflowRunConclusion -ne "success") { 84 | # TODO: Get workflow run logs 85 | #$workflowRunLogsUrl = $workflowRun.workflow_runs[0].logs_url 86 | #$workflowRunLogs = Invoke-RestMethod -Method GET -Uri $workflowRunLogsUrl -Headers $headers -StatusCodeVariable statusCode 87 | #$workflowRunLogsString = $workflowRunLogs | ConvertTo-Json -Depth 100 88 | #Write-Host "Workflow Run Logs:" 89 | #Write-Host $workflowRunLogsString 90 | throw "The workflow run did not complete successfully. Conclusion: $workflowRunConclusion" 91 | } 92 | } 93 | 94 | # Setup Variables 95 | $token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$personalAccessToken")) 96 | $headers = @{ 97 | "Authorization" = "Basic $token" 98 | "Accept" = "application/vnd.github+json" 99 | } 100 | 101 | # Run the Module in a retry loop 102 | $success = $false 103 | 104 | do { 105 | $retryCount++ 106 | try { 107 | # Get the workflow id 108 | Write-Host "Getting the workflow id" 109 | $workflowUrl = "https://api.github.com/repos/$organizationName/$repositoryName/actions/workflows/$workflowFileName" 110 | Write-Host "Workflow URL: $workflowUrl" 111 | $workflow = Invoke-RestMethod -Method GET -Uri $workflowUrl -Headers $headers -StatusCodeVariable statusCode 112 | if ($statusCode -ne 200) { 113 | throw "Failed to find the workflow." 114 | } 115 | $workflowId = $workflow.id 116 | Write-Host "Workflow ID: $workflowId" 117 | 118 | $workflowAction = "" 119 | 120 | if(!($skipDestroy)) { 121 | $workflowAction = "apply" 122 | } 123 | 124 | # Trigger the apply workflow 125 | Write-Host "Triggering the $workflowAction workflow" 126 | Invoke-Workflow -organizationName $organizationName -repositoryName $repositoryName -workflowId $workflowId -workflowAction $workflowAction -iac $iac -headers $headers 127 | Write-Host "$workflowAction workflow triggered successfully" 128 | 129 | # Wait for the apply workflow to complete 130 | Write-Host "Waiting for the $workflowAction workflow to complete" 131 | Wait-ForWorkflowRunToComplete -organizationName $organizationName -repositoryName $repositoryName -headers $headers 132 | Write-Host "$workflowAction workflow completed successfully" 133 | 134 | if($skipDestroy) { 135 | $success = $true 136 | break 137 | } 138 | 139 | $workflowAction = "destroy" 140 | 141 | # Trigger the destroy workflow 142 | Write-Host "Triggering the $workflowAction workflow" 143 | Invoke-Workflow -organizationName $organizationName -repositoryName $repositoryName -workflowId $workflowId -workflowAction $workflowAction -iac $iac -headers $headers 144 | Write-Host "$workflowAction workflow triggered successfully" 145 | 146 | # Wait for the apply workflow to complete 147 | Write-Host "Waiting for the $workflowAction workflow to complete" 148 | Wait-ForWorkflowRunToComplete -organizationName $organizationName -repositoryName $repositoryName -headers $headers 149 | Write-Host "$workflowAction workflow completed successfully" 150 | 151 | $success = $true 152 | } catch { 153 | Write-Host $_ 154 | Write-Host "Failed to trigger the workflow successfully, trying again..." 155 | } 156 | } while ($success -eq $false -and $retryCount -lt $maximumRetries) 157 | 158 | if ($success -eq $false) { 159 | throw "Failed to trigger the workflow after $maximumRetries attempts." 160 | } 161 | -------------------------------------------------------------------------------- /.github/workflows/docs-fmt-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docs & 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: Docs & fmt test 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup go 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: '1.20.x' 26 | cache-dependency-path: tests/go.sum 27 | 28 | - name: Setup Terraform 29 | uses: hashicorp/setup-terraform@v3 30 | with: 31 | terraform_version: latest 32 | terraform_wrapper: false 33 | 34 | - name: Install tools 35 | run: make tools 36 | 37 | - name: Check fmt and docs 38 | run: | 39 | echo "==> Running make fmt & make docs" 40 | make fmt 41 | make docs 42 | echo "==> Testing for changes to tracked files" 43 | CHANGES=$(git status -suno) 44 | if [ "$CHANGES" ]; then 45 | echo "Repository formatting or documentation is not correct." 46 | echo "Run 'make fmt && make docs' locally and commit the changes to fix." 47 | exit 1 48 | fi 49 | -------------------------------------------------------------------------------- /.github/workflows/pr-title-check.yml: -------------------------------------------------------------------------------- 1 | name: .Platform - Semantic PR Check 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR Title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | on: 5 | release: 6 | types: [published] 7 | workflow_dispatch: 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | release: 17 | name: Generate Release 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Zip and Tar 23 | run: | 24 | mkdir bootstrap 25 | cp -r .config bootstrap 26 | cp -r alz bootstrap 27 | cp -r modules bootstrap 28 | cd bootstrap 29 | tar -cvzf ../bootstrap_modules.tar.gz . 30 | zip -r ../bootstrap_modules.zip . 31 | 32 | - name: Upload Artifacts 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: bootstrap_modules 36 | path: | 37 | bootstrap_modules.tar.gz 38 | bootstrap_modules.zip 39 | 40 | - name: Release 41 | uses: softprops/action-gh-release@v2 42 | if: startsWith(github.ref, 'refs/tags/') 43 | with: 44 | files: | 45 | ./bootstrap_modules.tar.gz 46 | ./bootstrap_modules.zip 47 | -------------------------------------------------------------------------------- /.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: '16 22 * * 2' 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 | -------------------------------------------------------------------------------- /.github/workflows/super-linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linting 3 | on: 4 | pull_request: 5 | types: ['opened', 'synchronize'] 6 | merge_group: 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: linting-${{ github.event.pull_request.head.repo.full_name }}/${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | superlinter: 15 | name: super linter 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup Terraform 20 | uses: hashicorp/setup-terraform@v2 21 | with: 22 | terraform_version: latest 23 | terraform_wrapper: false 24 | - name: Run github/super-linter/slim 25 | uses: github/super-linter/slim@v5 26 | env: 27 | # Lint all code 28 | VALIDATE_ALL_CODEBASE: true 29 | FILTER_REGEX_EXCLUDE: '.*tests/vendor/.*' 30 | # Need to define main branch as default 31 | # is set to master in super-linter 32 | DEFAULT_BRANCH: main 33 | # Enable setting the status of each individual linter 34 | # run in the Checks section of a pull request 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | # The following linter types will be enabled: 37 | VALIDATE_BASH: true 38 | VALIDATE_BASH_EXEC: true 39 | VALIDATE_GITHUB_ACTIONS: true 40 | VALIDATE_JSON: true 41 | VALIDATE_MARKDOWN: true 42 | # VALIDATE_TERRAFORM_TERRASCAN: true # disabled for now as does not support TF 1.3 optional(type, default) 43 | VALIDATE_TERRAFORM_TFLINT: true 44 | VALIDATE_YAML: true 45 | # VALIDATE_GO: true # Disabled because it down not work :( 46 | # Additional settings: 47 | # If a shell script is not executable, the bash-exec 48 | # linter will report an error when set to true 49 | ERROR_ON_MISSING_EXEC_BIT: true 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | crash.*.log 11 | 12 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 13 | # password, private keys, and other secrets. These should not be part of version 14 | # control as they are data points which are potentially sensitive and subject 15 | # to change depending on the environment. 16 | *.tfvars 17 | *.tfvars.json 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | 32 | # Ignore CLI configuration files 33 | .terraformrc 34 | terraform.rc 35 | .terraform.lock.hcl 36 | terraform.tfvars 37 | .vscode/settings.json 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | @echo "==> Type make to run tasks" 3 | @echo 4 | @echo "Thing is one of:" 5 | @echo "docs fmt fmtcheck tfclean tools" 6 | 7 | docs: 8 | @echo "==> Updating documentation..." 9 | find . | egrep "\.md" | grep -v README.md | sort | while read f; do terrafmt fmt $$f; done 10 | 11 | fmt: 12 | @echo "==> Fixing Terraform code with terraform fmt..." 13 | terraform fmt -recursive 14 | @echo "==> Fixing embedded Terraform with terrafmt..." 15 | find . | egrep "\.md|\.tf" | grep -v README.md | sort | while read f; do terrafmt fmt $$f; done 16 | 17 | fmtcheck: 18 | @echo "==> Checking source code with gofmt..." 19 | @sh "$(CURDIR)/scripts/gofmtcheck.sh" 20 | @echo "==> Checking source code with terraform fmt..." 21 | terraform fmt -check -recursive 22 | 23 | tfclean: 24 | @echo "==> Cleaning terraform files..." 25 | find . -type d -name '.terraform' | xargs rm -vrf 26 | find . -type f -name 'tfplan' | xargs rm -vf 27 | find . -type f -name 'terraform.tfstate*' | xargs rm -vf 28 | find . -type f -name '.terraform.lock.hcl' | xargs rm -vf 29 | 30 | tools: 31 | go install github.com/katbyte/terrafmt@latest 32 | 33 | # Makefile targets are files, but we aren't using it like this, 34 | # so have to declare PHONY targets 35 | .PHONY: docs fmt fmtcheck tfclean tools 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Accelerator Bootstrap Modules 2 | 3 | [![End to End Tests](https://github.com/Azure/accelerator-bootstrap-modules/actions/workflows/end-to-end-test.yml/badge.svg)](https://github.com/Azure/accelerator-bootstrap-modules/actions/workflows/end-to-end-test.yml) 4 | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Azure/accelerator-bootstrap-modules/badge)](https://scorecard.dev/viewer/?uri=github.com/Azure/accelerator-bootstrap-modules) 5 | 6 | This repository contains the Terraform modules that are used to deploy the accelerator bootstrap environments. 7 | -------------------------------------------------------------------------------- /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) and [Xamarin](https://github.com/xamarin). 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://aka.ms/security.md/definition), 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://aka.ms/security.md/msrc/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://aka.ms/security.md/msrc/pgp). 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://aka.ms/security.md/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://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | ## Microsoft Support Polic 10 | 11 | Support for this PRODUCT is limited to the resources listed above. 12 | -------------------------------------------------------------------------------- /alz/azuredevops/data.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_client_config" "current" {} 2 | data "azurerm_subscription" "current" {} 3 | -------------------------------------------------------------------------------- /alz/azuredevops/locals.pipelines.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | pipelines = { 3 | ci = { 4 | pipeline_name = local.resource_names.version_control_system_pipeline_name_ci 5 | pipeline_file_name = "${local.target_folder_name}/${local.ci_file_name}" 6 | environment_keys = [ 7 | local.plan_key 8 | ] 9 | service_connection_keys = [ 10 | local.plan_key 11 | ] 12 | } 13 | cd = { 14 | pipeline_name = local.resource_names.version_control_system_pipeline_name_cd 15 | pipeline_file_name = "${local.target_folder_name}/${local.cd_file_name}" 16 | environment_keys = [ 17 | local.plan_key, 18 | local.apply_key 19 | ] 20 | service_connection_keys = [ 21 | local.plan_key, 22 | local.apply_key 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /alz/azuredevops/locals.tf: -------------------------------------------------------------------------------- 1 | # Resource Name Setup 2 | locals { 3 | resource_names = module.resource_names.resource_names 4 | } 5 | 6 | locals { 7 | root_parent_management_group_id = var.root_parent_management_group_id == "" ? data.azurerm_client_config.current.tenant_id : var.root_parent_management_group_id 8 | } 9 | 10 | locals { 11 | iac_terraform = "terraform" 12 | } 13 | 14 | locals { 15 | use_private_networking = var.use_self_hosted_agents && var.use_private_networking 16 | allow_storage_access_from_my_ip = local.use_private_networking && var.allow_storage_access_from_my_ip 17 | } 18 | 19 | locals { 20 | plan_key = "plan" 21 | apply_key = "apply" 22 | } 23 | 24 | locals { 25 | ci_file_name = "ci.yaml" 26 | cd_file_name = "cd.yaml" 27 | ci_template_file_name = "ci-template.yaml" 28 | cd_template_file_name = "cd-template.yaml" 29 | } 30 | 31 | locals { 32 | target_subscriptions = distinct([var.subscription_id_connectivity, var.subscription_id_identity, var.subscription_id_management]) 33 | } 34 | 35 | locals { 36 | managed_identities = { 37 | (local.plan_key) = local.resource_names.user_assigned_managed_identity_plan 38 | (local.apply_key) = local.resource_names.user_assigned_managed_identity_apply 39 | } 40 | 41 | federated_credentials = { 42 | (local.plan_key) = { 43 | user_assigned_managed_identity_key = local.plan_key 44 | federated_credential_subject = module.azure_devops.subjects[local.plan_key] 45 | federated_credential_issuer = module.azure_devops.issuers[local.plan_key] 46 | federated_credential_name = local.resource_names.user_assigned_managed_identity_federated_credentials_plan 47 | } 48 | (local.apply_key) = { 49 | user_assigned_managed_identity_key = local.apply_key 50 | federated_credential_subject = module.azure_devops.subjects[local.apply_key] 51 | federated_credential_issuer = module.azure_devops.issuers[local.apply_key] 52 | federated_credential_name = local.resource_names.user_assigned_managed_identity_federated_credentials_apply 53 | } 54 | } 55 | 56 | agent_container_instances = var.use_self_hosted_agents ? { 57 | agent_01 = { 58 | container_instance_name = local.resource_names.container_instance_01 59 | agent_name = local.resource_names.agent_01 60 | cpu = var.agent_container_cpu 61 | memory = var.agent_container_memory 62 | cpu_max = var.agent_container_cpu_max 63 | memory_max = var.agent_container_memory_max 64 | zones = var.agent_container_zone_support ? ["1"] : [] 65 | } 66 | agent_02 = { 67 | container_instance_name = local.resource_names.container_instance_02 68 | agent_name = local.resource_names.agent_02 69 | cpu = var.agent_container_cpu 70 | memory = var.agent_container_memory 71 | cpu_max = var.agent_container_cpu_max 72 | memory_max = var.agent_container_memory_max 73 | zones = var.agent_container_zone_support ? ["2"] : [] 74 | } 75 | } : {} 76 | } 77 | 78 | locals { 79 | environments = { 80 | (local.plan_key) = { 81 | environment_name = local.resource_names.version_control_system_environment_plan 82 | service_connection_name = local.resource_names.version_control_system_service_connection_plan 83 | service_connection_required_templates = [ 84 | "${local.target_folder_name}/${local.ci_template_file_name}", 85 | "${local.target_folder_name}/${local.cd_template_file_name}" 86 | ] 87 | } 88 | (local.apply_key) = { 89 | environment_name = local.resource_names.version_control_system_environment_apply 90 | service_connection_name = local.resource_names.version_control_system_service_connection_apply 91 | service_connection_required_templates = [ 92 | "${local.target_folder_name}/${local.cd_template_file_name}" 93 | ] 94 | } 95 | } 96 | } 97 | 98 | locals { 99 | starter_module_folder_path = var.module_folder_path_relative ? ("${path.module}/${var.module_folder_path}") : var.module_folder_path 100 | starter_root_module_folder_path = "${local.starter_module_folder_path}/${var.root_module_folder_relative_path}" 101 | } 102 | 103 | locals { 104 | agent_container_instance_dockerfile_url = "${var.agent_container_image_repository}#${var.agent_container_image_tag}:${var.agent_container_image_folder}" 105 | } 106 | 107 | locals { 108 | custom_role_definitions_bicep_names = { for key, value in var.custom_role_definitions_bicep : "custom_role_definition_bicep_${key}" => value.name } 109 | custom_role_definitions_terraform_names = { for key, value in var.custom_role_definitions_terraform : "custom_role_definition_terraform_${key}" => value.name } 110 | 111 | custom_role_definitions_bicep = { 112 | for key, value in var.custom_role_definitions_bicep : key => { 113 | name = local.resource_names["custom_role_definition_bicep_${key}"] 114 | description = value.description 115 | permissions = value.permissions 116 | } 117 | } 118 | 119 | custom_role_definitions_terraform = { 120 | for key, value in var.custom_role_definitions_terraform : key => { 121 | name = local.resource_names["custom_role_definition_terraform_${key}"] 122 | description = value.description 123 | permissions = value.permissions 124 | } 125 | } 126 | } 127 | 128 | locals { 129 | architecture_definition_name = var.architecture_definition_name 130 | has_architecture_definition = var.architecture_definition_name != null && var.architecture_definition_name != "" 131 | } 132 | -------------------------------------------------------------------------------- /alz/azuredevops/outputs.tf: -------------------------------------------------------------------------------- 1 | output "details" { 2 | description = "The details of the settings used" 3 | value = { 4 | iac_type = var.iac_type 5 | starter_module_name = var.starter_module_name 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/main/cd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: 3 | branches: 4 | include: 5 | - main 6 | 7 | parameters: 8 | - name: skipWhatIf 9 | displayName: Skip What If Check? 10 | type: boolean 11 | default: false 12 | %{ for script_file in script_files ~} 13 | - name: ${script_file.name} 14 | displayName: 'Run Step: ${script_file.displayName}' 15 | type: boolean 16 | default: true 17 | %{ endfor ~} 18 | - name: destroy 19 | displayName: "[DANGER!] Destroy? [DANGER!]" 20 | type: boolean 21 | default: false 22 | 23 | resources: 24 | repositories: 25 | - repository: templates 26 | type: git 27 | name: ${project_name}/${repository_name_templates} 28 | 29 | lockBehavior: sequential 30 | 31 | extends: 32 | template: ${cd_template_path}@templates 33 | parameters: 34 | skipWhatIf: $${{ parameters.skipWhatIf }} 35 | destroy: $${{ parameters.destroy }} 36 | %{ for script_file in script_files ~} 37 | ${script_file.name}: $${{ parameters.${script_file.name} }} 38 | %{ endfor ~} 39 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/main/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: 3 | - none 4 | 5 | resources: 6 | repositories: 7 | - repository: templates 8 | type: git 9 | name: ${project_name}/${repository_name_templates} 10 | 11 | lockBehavior: sequential 12 | 13 | extends: 14 | template: ${ci_template_path}@templates 15 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/templates/cd-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: skipWhatIf 4 | type: boolean 5 | default: false 6 | - name: destroy 7 | type: boolean 8 | default: false 9 | %{ for script_file in script_files ~} 10 | - name: ${script_file.name} 11 | type: boolean 12 | default: true 13 | %{ endfor ~} 14 | 15 | stages: 16 | - stage: whatif 17 | displayName: What If 18 | condition: eq($${{ parameters.skipWhatIf }}, false) 19 | variables: 20 | - group: ${variable_group_name} 21 | - name: parametersFileName 22 | value: parameters.json 23 | 24 | jobs: 25 | - deployment: whatif 26 | displayName: What If with Bicep 27 | pool: 28 | ${agent_pool_configuration} 29 | environment: ${environment_name_plan} 30 | timeoutInMinutes: 0 31 | strategy: 32 | runOnce: 33 | deploy: 34 | steps: 35 | - checkout: self 36 | displayName: Checkout Bicep Module 37 | 38 | - template: helpers/bicep-variables.yaml 39 | parameters: 40 | parametersFileName: $(parametersFileName) 41 | 42 | %{ for on_demand_folder in on_demand_folders ~} 43 | - template: helpers/bicep-on-demand-folder.yaml 44 | parameters: 45 | repository: "${on_demand_folder_repository}" 46 | releaseArtifactName: "${on_demand_folder_artifact_name}" 47 | releaseVersion: "$(RELEASE_VERSION)" 48 | sourcePath: "${on_demand_folder.source}" 49 | targetPath: "${on_demand_folder.target}" 50 | 51 | %{ endfor ~} 52 | 53 | - $${{ if eq(parameters.destroy, false) }}: 54 | - template: helpers/bicep-installer.yaml 55 | parameters: 56 | serviceConnection: '${service_connection_name_plan}' 57 | 58 | - template: helpers/bicep-templates.yaml 59 | parameters: 60 | serviceConnection: '${service_connection_name_plan}' 61 | %{ for script_file in script_files ~} 62 | ${script_file.name}: $${{ parameters.${script_file.name} }} 63 | %{ endfor ~} 64 | - $${{ if eq(parameters.destroy, true) }}: 65 | - template: helpers/bicep-installer.yaml 66 | parameters: 67 | serviceConnection: '${service_connection_name_plan}' 68 | 69 | - template: helpers/bicep-destroy.yaml 70 | parameters: 71 | serviceConnection: '${service_connection_name_plan}' 72 | 73 | - stage: deploy 74 | displayName: Deploy 75 | dependsOn: whatif 76 | condition: not(or(failed(), canceled())) 77 | variables: 78 | - group: ${variable_group_name} 79 | - name: parametersFileName 80 | value: parameters.json 81 | 82 | jobs: 83 | - deployment: deploy 84 | displayName: Deploy with Bicep 85 | pool: 86 | ${agent_pool_configuration} 87 | environment: ${environment_name_apply} 88 | timeoutInMinutes: 0 89 | strategy: 90 | runOnce: 91 | deploy: 92 | steps: 93 | - checkout: self 94 | displayName: Checkout Bicep Module 95 | 96 | - template: helpers/bicep-variables.yaml 97 | parameters: 98 | parametersFileName: $(parametersFileName) 99 | 100 | %{ for on_demand_folder in on_demand_folders ~} 101 | - template: helpers/bicep-on-demand-folder.yaml 102 | parameters: 103 | repository: "${on_demand_folder_repository}" 104 | releaseArtifactName: "${on_demand_folder_artifact_name}" 105 | releaseVersion: "$(RELEASE_VERSION)" 106 | sourcePath: "${on_demand_folder.source}" 107 | targetPath: "${on_demand_folder.target}" 108 | 109 | %{ endfor ~} 110 | 111 | - $${{ if eq(parameters.destroy, false) }}: 112 | - template: helpers/bicep-installer.yaml 113 | parameters: 114 | serviceConnection: '${service_connection_name_apply}' 115 | 116 | - template: helpers/bicep-templates.yaml 117 | parameters: 118 | serviceConnection: '${service_connection_name_apply}' 119 | whatIfEnabled: false 120 | %{ for script_file in script_files ~} 121 | ${script_file.name}: $${{ parameters.${script_file.name} }} 122 | %{ endfor ~} 123 | - $${{ if eq(parameters.destroy, true) }}: 124 | - template: helpers/bicep-installer.yaml 125 | parameters: 126 | serviceConnection: '${service_connection_name_apply}' 127 | 128 | - template: helpers/bicep-destroy.yaml 129 | parameters: 130 | serviceConnection: '${service_connection_name_apply}' 131 | whatIfEnabled: false 132 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/templates/ci-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | stages: 3 | - stage: validate 4 | displayName: Validation Bicep 5 | variables: 6 | - group: ${variable_group_name} 7 | - name: parametersFileName 8 | value: parameters.json 9 | 10 | jobs: 11 | - job: validate 12 | displayName: Validate Bicep 13 | pool: 14 | ${agent_pool_configuration} 15 | steps: 16 | - checkout: self 17 | displayName: Checkout Repo 18 | 19 | - template: helpers/bicep-installer.yaml 20 | parameters: 21 | serviceConnection: '${service_connection_name_plan}' 22 | 23 | - pwsh: | 24 | if (Test-Path -Path ./custom-modules/*) 25 | { 26 | echo "##vso[task.setvariable variable=CUSTOM_MODULES;]true" 27 | echo "Set CUSTOM_MODULES to true" 28 | } 29 | else 30 | { 31 | echo "Set CUSTOM_MODULES to false" 32 | } 33 | workingDirectory: config 34 | displayName: Check for Custom Modules 35 | 36 | - pwsh: | 37 | $output = @() 38 | Get-ChildItem -Recurse -Filter '*.bicep' | ForEach-Object { 39 | Write-Information "==> Attempting Bicep Build For File: $_" -InformationAction Continue 40 | $bicepOutput = bicep build $_.FullName 2>&1 41 | if ($LastExitCode -ne 0) 42 | { 43 | foreach ($item in $bicepOutput) { 44 | $output += "$($item) `r`n" 45 | } 46 | } 47 | else 48 | { 49 | echo "Bicep Build Successful for File: $_" 50 | } 51 | } 52 | if ($output.length -gt 0) { 53 | throw $output 54 | } 55 | workingDirectory: config/custom-modules 56 | condition: eq(variables['CUSTOM_MODULES'], 'true') 57 | displayName: Bicep Build & Lint All Custom Modules 58 | 59 | - deployment: whatif 60 | displayName: What If Deploy with Bicep 61 | pool: 62 | ${agent_pool_configuration} 63 | environment: ${environment_name_plan} 64 | timeoutInMinutes: 0 65 | strategy: 66 | runOnce: 67 | deploy: 68 | steps: 69 | - checkout: self 70 | displayName: Checkout Bicep Module 71 | 72 | - template: helpers/bicep-installer.yaml 73 | parameters: 74 | serviceConnection: '${service_connection_name_plan}' 75 | 76 | - template: helpers/bicep-variables.yaml 77 | parameters: 78 | parametersFileName: $(parametersFileName) 79 | 80 | %{ for on_demand_folder in on_demand_folders ~} 81 | - template: helpers/bicep-on-demand-folder.yaml 82 | parameters: 83 | repository: "${on_demand_folder_repository}" 84 | releaseArtifactName: "${on_demand_folder_artifact_name}" 85 | releaseVersion: "$(RELEASE_VERSION)" 86 | sourcePath: "${on_demand_folder.source}" 87 | targetPath: "${on_demand_folder.target}" 88 | 89 | %{ endfor ~} 90 | 91 | - template: helpers/bicep-templates.yaml 92 | parameters: 93 | serviceConnection: '${service_connection_name_plan}' 94 | whatIfEnabled: true 95 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/templates/helpers/bicep-destroy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: serviceConnection 4 | type: string 5 | - name: whatIfEnabled 6 | type: boolean 7 | default: true 8 | 9 | steps: 10 | - task: AzurePowerShell@5 11 | displayName: "[DANGER!] DESTROY! [DANGER!]" 12 | inputs: 13 | azureSubscription: $${{ parameters.serviceConnection }} 14 | pwsh: true 15 | azurePowerShellVersion: LatestVersion 16 | ScriptType: "FilePath" 17 | ScriptPath: "${destroy_script_path}" 18 | ScriptArguments: "-WhatIfEnabled $$${{ parameters.whatIfEnabled }}" 19 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/templates/helpers/bicep-installer.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: serviceConnection 4 | type: string 5 | 6 | steps: 7 | - pwsh: | 8 | $latestVersion = (Find-Module AZ).Version 9 | Write-Host "Latest AZ PowerShell Version: $latestVersion" 10 | echo "##vso[task.setvariable variable=lastestPowerShellAzVersion;]$latestVersion" 11 | 12 | $installedModule = Get-InstalledModule -Name AZ -ErrorAction SilentlyContinue 13 | $installedVersion = $installedModule.Version 14 | Write-Host "Installed AZ PowerShell Version: $installedVersion" 15 | 16 | $runPowerShellAzUpgrade = $true 17 | 18 | if($installedVersion -ne $latestVersion) { 19 | Write-Host "Az PowerShell is not at the latest version, running upgrade step..." 20 | } else { 21 | Write-Host "Az PowerShell is already at the latest version, skipping upgrade step..." 22 | $runPowerShellAzUpgrade = $false 23 | } 24 | echo "##vso[task.setvariable variable=runPowerShellAzUpgrade;]$runPowerShellAzUpgrade" 25 | displayName: Check Az PowerShell Version 26 | 27 | - task: AzurePowerShell@5 28 | displayName: "Upgrade AZ PowerShell" 29 | condition: and(succeeded(), eq(variables['runPowerShellAzUpgrade'], 'true')) 30 | inputs: 31 | azureSubscription: $${{ parameters.serviceConnection }} 32 | pwsh: true 33 | preferredAzurePowerShellVersion: $(lastestPowerShellAzVersion) 34 | ScriptType: "InlineScript" 35 | Inline: | 36 | Write-Host "Attempted Upgrade of AZ PowerShell to $env:lastestPowerShellAzVersion" 37 | 38 | - pwsh: | 39 | $TOOLS_PATH = $env:TOOLS_PATH 40 | $installDir = Join-Path -Path $TOOLS_PATH -ChildPath "bicep" 41 | 42 | $toolFileName = "bicep" 43 | $toolFilePath = Join-Path -Path $installDir -ChildPath $toolFileName 44 | 45 | if(!(Test-Path $installDir)) { 46 | New-Item -ItemType Directory -Path $installDir | Out-String | Write-Verbose 47 | } 48 | 49 | $url = "https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64" 50 | 51 | Invoke-WebRequest -Uri $url -OutFile "$toolFilePath" | Out-String | Write-Verbose 52 | 53 | $isExecutable = $(test -x $toolFilePath; 0 -eq $LASTEXITCODE) 54 | if(!($isExecutable)) { 55 | chmod +x $toolFilePath 56 | } 57 | 58 | $env:PATH = "$($installDir):$env:PATH" 59 | 60 | Write-Host "##vso[task.setvariable variable=PATH]$env:PATH" 61 | 62 | bicep --version 63 | 64 | Write-Host "Installed Latest Bicep Version" 65 | 66 | displayName: Install Bicep 67 | env: 68 | TOOLS_PATH: $(Agent.ToolsDirectory) 69 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/templates/helpers/bicep-on-demand-folder.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: repository 4 | type: string 5 | - name: releaseArtifactName 6 | type: string 7 | - name: releaseVersion 8 | type: string 9 | - name: sourcePath 10 | type: string 11 | - name: targetPath 12 | type: string 13 | 14 | steps: 15 | - pwsh: | 16 | $repository = "$${{ parameters.repository }}" 17 | $releaseArtifactName = "$${{ parameters.releaseArtifactName }}" 18 | $releaseVersion = "$${{ parameters.releaseVersion }}" 19 | $sourcePath = "$${{ parameters.sourcePath }}" 20 | $targetPath = "$${{ parameters.targetPath }}" 21 | 22 | Write-Host "Repository: $repository" 23 | Write-Host "Release Artifact Name: $releaseArtifactName" 24 | Write-Host "Release Version: $releaseVersion" 25 | Write-Host "Source Path: $sourcePath" 26 | Write-Host "Target Path: $targetPath" 27 | 28 | $repoOrgPlusRepo = $repository.Split("/")[-2..-1] -join "/" 29 | 30 | $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/tags/$releaseVersion" 31 | if($releaseVersion -eq "latest") { 32 | $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/latest" 33 | } 34 | $releaseData = Invoke-RestMethod $repoReleaseUrl -SkipHttpErrorCheck -StatusCodeVariable "statusCode" 35 | Write-Verbose "Status code: $statusCode" 36 | 37 | if($statusCode -eq 404) { 38 | Write-Error "The release $releaseVersion does not exist in the GitHub repository $repository - $repoReleaseUrl" 39 | throw "The release $releaseVersion does not exist in the GitHub repository $repository - $repoReleaseUrl" 40 | } 41 | 42 | # Handle transient errors like throttling 43 | if($statusCode -ge 400 -and $statusCode -le 599) { 44 | Write-Host "Retrying as got the Status Code $statusCode, which may be a tranisent error." -ForegroundColor Yellow 45 | $releaseData = Invoke-RestMethod $repoReleaseUrl -RetryIntervalSec 3 -MaximumRetryCount 100 46 | } 47 | 48 | if($statusCode -ne 200) { 49 | throw "Unable to query repository version, please check your internet connection and try again..." 50 | } 51 | 52 | $releaseArtifactUrl = $releaseData.assets | Where-Object { $_.name -eq $releaseArtifactName } | Select-Object -ExpandProperty browser_download_url 53 | 54 | $tempFolder = "./download" 55 | 56 | New-Item -ItemType Directory -Path $tempFolder | Out-String | Write-Verbose 57 | $targetPathForZip = "$tempFolder/$releaseArtifactName" 58 | Invoke-WebRequest -Uri $releaseArtifactUrl -OutFile $targetPathForZip -RetryIntervalSec 3 -MaximumRetryCount 100 | Out-String | Write-Verbose 59 | 60 | $targetPathForExtractedZip = "$tempFolder/extracted" 61 | 62 | Expand-Archive -Path $targetPathForZip -DestinationPath $targetPathForExtractedZip | Out-String | Write-Verbose 63 | 64 | $sourceFolderPath = "$($targetPathForExtractedZip)/$sourcePath/*" 65 | $targetFolderPath = "./$targetPath" 66 | 67 | Write-Host "Copying extracted files from $sourceFolderPath to $targetFolderPath" 68 | New-Item -ItemType Directory -Path $targetFolderPath | Out-String | Write-Verbose 69 | Copy-Item -Path $sourceFolderPath -Destination $targetFolderPath -Recurse -Force | Out-String | Write-Verbose 70 | 71 | Remove-Item -Path $tempFolder -Force -Recurse 72 | Write-Host "Successfully copied the files from the release artifact to the target path $targetPath..." 73 | 74 | displayName: Get On Demand Folder 75 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/templates/helpers/bicep-templates.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: serviceConnection 4 | type: string 5 | - name: whatIfEnabled 6 | type: boolean 7 | default: true 8 | %{ for script_file in script_files ~} 9 | - name: ${script_file.name} 10 | type: boolean 11 | default: true 12 | %{ endfor ~} 13 | 14 | steps: 15 | - template: bicep-deploy.yaml 16 | parameters: 17 | serviceConnection: $${{ parameters.serviceConnection }} 18 | whatIfEnabled: $${{ parameters.whatIfEnabled }} 19 | scriptFiles: 20 | %{ for script_file in script_files ~} 21 | - displayName: "${script_file.displayName}" 22 | templateFilePath: "${script_file.templateFilePath}" 23 | templateParametersFilePath: "${script_file.templateParametersFilePath}" 24 | managementGroupId: ${script_file.managementGroupIdVariable} 25 | subscriptionId: ${script_file.subscriptionIdVariable} 26 | resourceGroupName: ${script_file.resourceGroupNameVariable} 27 | location: "$(LOCATION)" 28 | deploymentType: "${script_file.deploymentType}" 29 | firstRunWhatIf: ${script_file.firstRunWhatIf} 30 | runStep: $${{ parameters.${script_file.name} }} 31 | 32 | %{ endfor ~} 33 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/bicep/templates/helpers/bicep-variables.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: parametersFileName 4 | type: string 5 | 6 | steps: 7 | - pwsh: | 8 | $fileName = "$${{ parameters.parametersFileName }}" 9 | Write-Host "Getting variables from $fileName" 10 | $json = Get-Content -Path $fileName | ConvertFrom-Json 11 | 12 | foreach ($key in $json.PSObject.Properties) { 13 | $envVarName = $key.Name 14 | $envVarValue = $key.Value 15 | echo "##vso[task.setvariable variable=$envVarName;]$envVarValue" 16 | echo "Set $envVarName to $envVarValue" 17 | } 18 | displayName: Import Variables from File 19 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/terraform/main/cd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: 3 | branches: 4 | include: 5 | - main 6 | 7 | resources: 8 | repositories: 9 | - repository: templates 10 | type: git 11 | name: ${project_name}/${repository_name_templates} 12 | 13 | parameters: 14 | - name: terraform_action 15 | displayName: Terraform Action to perform 16 | type: string 17 | default: 'apply' 18 | values: 19 | - 'apply' 20 | - 'destroy' 21 | - name: terraform_cli_version 22 | displayName: Terraform CLI Version 23 | type: string 24 | default: 'latest' 25 | 26 | lockBehavior: sequential 27 | 28 | extends: 29 | template: ${cd_template_path}@templates 30 | parameters: 31 | terraform_action: $${{ parameters.terraform_action }} 32 | root_module_folder_relative_path: ${root_module_folder_relative_path} 33 | terraform_cli_version: $${{ coalesce(parameters.terraform_cli_version, 'latest') }} 34 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/terraform/main/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: 3 | - none 4 | 5 | resources: 6 | repositories: 7 | - repository: templates 8 | type: git 9 | name: ${project_name}/${repository_name_templates} 10 | 11 | parameters: 12 | - name: terraform_cli_version 13 | displayName: Terraform CLI Version 14 | type: string 15 | default: 'latest' 16 | 17 | lockBehavior: sequential 18 | 19 | extends: 20 | template: ${ci_template_path}@templates 21 | parameters: 22 | root_module_folder_relative_path: ${root_module_folder_relative_path} 23 | terraform_cli_version: $${{ coalesce(parameters.terraform_cli_version, 'latest') }} 24 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/terraform/templates/cd-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: terraform_action 4 | default: 'apply' 5 | - name: root_module_folder_relative_path 6 | default: '.' 7 | - name: terraform_cli_version 8 | default: 'latest' 9 | 10 | stages: 11 | - stage: plan 12 | displayName: Plan 13 | variables: 14 | - group: ${variable_group_name} 15 | - name: 'selfHostedAgent' 16 | value: '${self_hosted_agent}' 17 | jobs: 18 | - deployment: plan 19 | displayName: Plan with Terraform 20 | pool: 21 | ${agent_pool_configuration} 22 | environment: ${environment_name_plan} 23 | timeoutInMinutes: 0 24 | strategy: 25 | runOnce: 26 | deploy: 27 | steps: 28 | - checkout: self 29 | displayName: Checkout Terraform Module 30 | - template: helpers/terraform-installer.yaml 31 | parameters: 32 | terraformVersion: $${{ parameters.terraform_cli_version }} 33 | - template: helpers/terraform-init.yaml 34 | parameters: 35 | serviceConnection: '${service_connection_name_plan}' 36 | backendAzureResourceGroupName: $(BACKEND_AZURE_RESOURCE_GROUP_NAME) 37 | backendAzureStorageAccountName: $(BACKEND_AZURE_STORAGE_ACCOUNT_NAME) 38 | backendAzureStorageAccountContainerName: $(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME) 39 | root_module_folder_relative_path: $${{ parameters.root_module_folder_relative_path }} 40 | - template: helpers/terraform-plan.yaml 41 | parameters: 42 | terraform_action: $${{ parameters.terraform_action }} 43 | serviceConnection: '${service_connection_name_plan}' 44 | root_module_folder_relative_path: $${{ parameters.root_module_folder_relative_path }} 45 | - task: CopyFiles@2 46 | displayName: Create Module Artifact 47 | inputs: 48 | SourceFolder: '$(Build.SourcesDirectory)' 49 | Contents: | 50 | **/* 51 | !.terraform/**/* 52 | !.git/**/* 53 | !.pipelines/**/* 54 | !**/.terraform/**/* 55 | !**/.git/**/* 56 | !**/.pipelines/**/* 57 | TargetFolder: '$(Build.ArtifactsStagingDirectory)' 58 | CleanTargetFolder: true 59 | OverWrite: true 60 | - task: PublishPipelineArtifact@1 61 | displayName: Publish Module Artifact 62 | inputs: 63 | targetPath: '$(Build.ArtifactsStagingDirectory)' 64 | artifact: 'module' 65 | publishLocation: 'pipeline' 66 | - pwsh: | 67 | terraform ` 68 | -chdir="$${{ parameters.root_module_folder_relative_path }}" ` 69 | show ` 70 | tfplan 71 | displayName: Show the Plan for Review 72 | - stage: apply 73 | displayName: Apply 74 | dependsOn: plan 75 | variables: 76 | - group: ${variable_group_name} 77 | - name: 'selfHostedAgent' 78 | value: '${self_hosted_agent}' 79 | jobs: 80 | - deployment: apply 81 | displayName: Apply with Terraform 82 | pool: 83 | ${agent_pool_configuration} 84 | environment: ${environment_name_apply} 85 | timeoutInMinutes: 0 86 | strategy: 87 | runOnce: 88 | deploy: 89 | steps: 90 | - download: none 91 | - task: DownloadPipelineArtifact@2 92 | displayName: Download Module Artifact 93 | inputs: 94 | buildType: 'current' 95 | artifactName: 'module' 96 | targetPath: '$(Build.SourcesDirectory)' 97 | - template: helpers/terraform-installer.yaml 98 | parameters: 99 | terraformVersion: $${{ parameters.terraform_cli_version }} 100 | - template: helpers/terraform-init.yaml 101 | parameters: 102 | serviceConnection: '${service_connection_name_apply}' 103 | backendAzureResourceGroupName: $(BACKEND_AZURE_RESOURCE_GROUP_NAME) 104 | backendAzureStorageAccountName: $(BACKEND_AZURE_STORAGE_ACCOUNT_NAME) 105 | backendAzureStorageAccountContainerName: $(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME) 106 | root_module_folder_relative_path: $${{ parameters.root_module_folder_relative_path }} 107 | - template: helpers/terraform-apply.yaml 108 | parameters: 109 | terraform_action: $${{ parameters.terraform_action }} 110 | serviceConnection: '${service_connection_name_apply}' 111 | root_module_folder_relative_path: $${{ parameters.root_module_folder_relative_path }} 112 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/terraform/templates/ci-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: root_module_folder_relative_path 4 | default: '.' 5 | - name: terraform_cli_version 6 | default: 'latest' 7 | 8 | stages: 9 | - stage: validate 10 | displayName: Validation Terraform 11 | variables: 12 | - group: ${variable_group_name} 13 | - name: 'selfHostedAgent' 14 | value: '${self_hosted_agent}' 15 | jobs: 16 | - job: validate 17 | displayName: Validate Terraform 18 | pool: 19 | ${agent_pool_configuration} 20 | steps: 21 | - template: helpers/terraform-installer.yaml 22 | parameters: 23 | terraformVersion: $${{ parameters.terraform_cli_version }} 24 | - pwsh: | 25 | terraform ` 26 | -chdir="$${{ parameters.root_module_folder_relative_path }}" ` 27 | fmt ` 28 | -check 29 | displayName: Terraform Format Check 30 | - pwsh: | 31 | terraform ` 32 | -chdir="$${{ parameters.root_module_folder_relative_path }}" ` 33 | init ` 34 | -backend=false 35 | displayName: Terraform Init 36 | - pwsh: | 37 | terraform ` 38 | -chdir="$${{ parameters.root_module_folder_relative_path }}" ` 39 | validate 40 | displayName: Terraform Validate 41 | - deployment: plan 42 | dependsOn: validate 43 | displayName: Validate Terraform Plan 44 | pool: 45 | ${agent_pool_configuration} 46 | environment: ${environment_name_plan} 47 | timeoutInMinutes: 0 48 | strategy: 49 | runOnce: 50 | deploy: 51 | steps: 52 | - checkout: self 53 | displayName: Checkout Terraform Module 54 | - template: helpers/terraform-installer.yaml 55 | parameters: 56 | terraformVersion: $${{ parameters.terraform_cli_version }} 57 | - template: helpers/terraform-init.yaml 58 | parameters: 59 | serviceConnection: '${service_connection_name_plan}' 60 | backendAzureResourceGroupName: $(BACKEND_AZURE_RESOURCE_GROUP_NAME) 61 | backendAzureStorageAccountName: $(BACKEND_AZURE_STORAGE_ACCOUNT_NAME) 62 | backendAzureStorageAccountContainerName: $(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME) 63 | root_module_folder_relative_path: $${{ parameters.root_module_folder_relative_path }} 64 | - template: helpers/terraform-plan.yaml 65 | parameters: 66 | serviceConnection: '${service_connection_name_plan}' 67 | root_module_folder_relative_path: $${{ parameters.root_module_folder_relative_path }} 68 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/terraform/templates/helpers/terraform-apply.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: terraform_action 4 | default: 'apply' 5 | - name: serviceConnection 6 | - name: root_module_folder_relative_path 7 | default: '.' 8 | 9 | steps: 10 | - task: AzureCLI@2 11 | displayName: Terraform Apply for $${{ coalesce(parameters.terraform_action, 'Apply') }} 12 | inputs: 13 | azureSubscription: $${{ parameters.serviceConnection }} 14 | scriptType: pscore 15 | scriptLocation: inlineScript 16 | addSpnToEnvironment: true 17 | inlineScript: | 18 | # Get settings from service connection 19 | az account show 2>$null | ConvertFrom-Json | Set-Variable account 20 | $clientId = $account.user.name 21 | $oidcToken = $env:idToken # requires addSpnToEnvironment: true 22 | $subscriptionId = $account.id 23 | $tenantId = $account.tenantId 24 | 25 | $env:ARM_TENANT_ID = $account.tenantId 26 | $env:ARM_SUBSCRIPTION_ID = $account.id 27 | $env:ARM_OIDC_TOKEN = $oidcToken 28 | $env:ARM_USE_OIDC = "true" 29 | $env:ARM_CLIENT_ID = $clientId 30 | $env:ARM_USE_AZUREAD = "true" 31 | $env:AZAPI_RETRY_GET_AFTER_PUT_MAX_TIME = "60m" # Accounts for eventually consistent management group permissions propagation 32 | 33 | # Run Terraform Apply 34 | $command = "terraform" 35 | $arguments = @() 36 | $arguments += "-chdir=$${{ parameters.root_module_folder_relative_path }}" 37 | $arguments += "apply" 38 | $arguments += "-auto-approve" 39 | $arguments += "tfplan" 40 | Write-Host "Running: $command $arguments" 41 | & $command $arguments 42 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/terraform/templates/helpers/terraform-init.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: serviceConnection 4 | - name: backendAzureResourceGroupName 5 | - name: backendAzureStorageAccountName 6 | - name: backendAzureStorageAccountContainerName 7 | - name: backendAzureStorageAccountContainerKeyName 8 | default: terraform.tfstate 9 | - name: root_module_folder_relative_path 10 | default: '.' 11 | 12 | steps: 13 | - task: AzureCLI@2 14 | displayName: 'Terraform Init' 15 | inputs: 16 | azureSubscription: $${{ parameters.serviceConnection }} 17 | scriptType: pscore 18 | scriptLocation: inlineScript 19 | addSpnToEnvironment: true 20 | inlineScript: | 21 | # Get settings from service connection 22 | az account show 2>$null | ConvertFrom-Json | Set-Variable account 23 | $clientId = $account.user.name 24 | $oidcToken = $env:idToken # requires addSpnToEnvironment: true 25 | $subscriptionId = $account.id 26 | $tenantId = $account.tenantId 27 | 28 | $env:ARM_TENANT_ID = $tenantId 29 | $env:ARM_SUBSCRIPTION_ID = $subscriptionId 30 | $env:ARM_OIDC_TOKEN = $oidcToken 31 | $env:ARM_USE_OIDC = "true" 32 | $env:ARM_CLIENT_ID = $clientId 33 | $env:ARM_USE_AZUREAD = "true" 34 | 35 | $arguments = @() 36 | $arguments += "-chdir=$${{ parameters.root_module_folder_relative_path }}" 37 | $arguments += "init" 38 | $arguments += "-backend-config=storage_account_name=$($env:BACKEND_AZURE_STORAGE_ACCOUNT_NAME)" 39 | $arguments += "-backend-config=container_name=$($env:BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME)" 40 | $arguments += "-backend-config=key=$($env:BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_KEY_NAME)" 41 | $arguments += "-backend-config=resource_group_name=$($env:BACKEND_AZURE_RESOURCE_GROUP_NAME)" 42 | 43 | # Run terraform init 44 | $command = "terraform" 45 | Write-Host "Running: $command $arguments" 46 | & $command $arguments 47 | 48 | env: 49 | BACKEND_AZURE_RESOURCE_GROUP_NAME: $${{ parameters.backendAzureResourceGroupName }} 50 | BACKEND_AZURE_STORAGE_ACCOUNT_NAME: $${{ parameters.backendAzureStorageAccountName }} 51 | BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME: $${{ parameters.backendAzureStorageAccountContainerName }} 52 | BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_KEY_NAME: $${{ parameters.backendAzureStorageAccountContainerKeyName }} 53 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/terraform/templates/helpers/terraform-installer.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: terraformVersion 4 | default: 'latest' 5 | 6 | steps: 7 | - pwsh: | 8 | $TF_VERSION = $env:TF_VERSION 9 | $TOOLS_PATH = $env:TOOLS_PATH 10 | 11 | $release = $null 12 | 13 | if($TF_VERSION -eq "latest") { 14 | $versionResponse = Invoke-WebRequest -Uri "https://api.releases.hashicorp.com/v1/releases/terraform?limit=20" 15 | if($versionResponse.StatusCode -ne "200") { 16 | throw "Unable to query Terraform version, please check your internet connection and try again..." 17 | } 18 | $releases = ($versionResponse).Content | ConvertFrom-Json | Where-Object -Property is_prerelease -EQ $false 19 | $release = $releases[0] 20 | $TF_VERSION = $releases[0].version 21 | } else { 22 | $versionResponse = Invoke-WebRequest -Uri "https://api.releases.hashicorp.com/v1/releases/terraform/$($TF_VERSION)" 23 | if($versionResponse.StatusCode -ne "200") { 24 | throw "Unable to query Terraform version, please check the supplied version and try again..." 25 | } 26 | $release = ($versionResponse).Content | ConvertFrom-Json 27 | } 28 | 29 | $commandDetails = Get-Command -Name terraform -ErrorAction SilentlyContinue 30 | if($commandDetails) { 31 | Write-Host "Terraform already installed in $($commandDetails.Path), checking version" 32 | $installedVersion = terraform version -json | ConvertFrom-Json 33 | Write-Host "Installed version: $($installedVersion.terraform_version) on $($installedVersion.platform)" 34 | if($installedVersion.terraform_version -eq $TF_VERSION) { 35 | Write-Host "Installed version matches required version $TF_VERSION, skipping install" 36 | return 37 | } 38 | } 39 | 40 | $unzipdir = Join-Path -Path $TOOLS_PATH -ChildPath "terraform_$TF_VERSION" 41 | if (Test-Path $unzipdir) { 42 | Write-Host "Terraform $TF_VERSION already installed." 43 | if($os -eq "windows") { 44 | $env:PATH = "$($unzipdir);$env:PATH" 45 | } else { 46 | $env:PATH = "$($unzipdir):$env:PATH" 47 | } 48 | Write-Host "##vso[task.setvariable variable=PATH]$env:PATH" 49 | return 50 | } 51 | 52 | $os = "" 53 | if ($IsWindows) { 54 | $os = "windows" 55 | } 56 | if($IsLinux) { 57 | $os = "linux" 58 | } 59 | if($IsMacOS) { 60 | $os = "darwin" 61 | } 62 | 63 | # Enum values can be seen here: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.architecture?view=net-7.0#fields 64 | $architecture = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLower() 65 | 66 | if($architecture -eq "x64") { 67 | $architecture = "amd64" 68 | } 69 | if($architecture -eq "x86") { 70 | $architecture = "386" 71 | } 72 | 73 | $osAndArchitecture = "$($os)_$($architecture)" 74 | 75 | $supportedOsAndArchitectures = @( 76 | "darwin_amd64", 77 | "darwin_arm64", 78 | "linux_386", 79 | "linux_amd64", 80 | "linux_arm64", 81 | "windows_386", 82 | "windows_amd64" 83 | ) 84 | 85 | if($supportedOsAndArchitectures -notcontains $osAndArchitecture) { 86 | Write-Error "Unsupported OS and architecture combination: $osAndArchitecture" 87 | exit 1 88 | } 89 | 90 | $zipfilePath = "$unzipdir.zip" 91 | 92 | $url = $release.builds | Where-Object { $_.arch -eq $architecture -and $_.os -eq $os } | Select-Object -First 1 -ExpandProperty url 93 | 94 | if(!(Test-Path $TOOLS_PATH)) { 95 | New-Item -ItemType Directory -Path $TOOLS_PATH| Out-String | Write-Verbose 96 | } 97 | 98 | Invoke-WebRequest -Uri $url -OutFile "$zipfilePath" | Out-String | Write-Verbose 99 | 100 | Expand-Archive -Path $zipfilePath -DestinationPath $unzipdir 101 | 102 | $toolFileName = "terraform" 103 | 104 | if($os -eq "windows") { 105 | $toolFileName = "$($toolFileName).exe" 106 | } 107 | 108 | $toolFilePath = Join-Path -Path $unzipdir -ChildPath $toolFileName 109 | 110 | if($os -ne "windows") { 111 | $isExecutable = $(test -x $toolFilePath; 0 -eq $LASTEXITCODE) 112 | if(!($isExecutable)) { 113 | chmod +x $toolFilePath 114 | } 115 | } 116 | 117 | if($os -eq "windows") { 118 | $env:PATH = "$($unzipdir);$env:PATH" 119 | } else { 120 | $env:PATH = "$($unzipdir):$env:PATH" 121 | } 122 | Write-Host "##vso[task.setvariable variable=PATH]$env:PATH" 123 | Remove-Item $zipfilePath 124 | Write-Host "Installed Terraform version $TF_VERSION" 125 | 126 | displayName: Install Terraform 127 | env: 128 | TF_VERSION: $${{ parameters.terraformVersion }} 129 | TOOLS_PATH: $(Agent.ToolsDirectory) 130 | -------------------------------------------------------------------------------- /alz/azuredevops/pipelines/terraform/templates/helpers/terraform-plan.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parameters: 3 | - name: terraform_action 4 | default: 'apply' 5 | - name: serviceConnection 6 | - name: root_module_folder_relative_path 7 | default: '.' 8 | 9 | steps: 10 | - task: AzureCLI@2 11 | displayName: Terraform Plan for $${{ coalesce(parameters.terraform_action, 'Apply') }} 12 | inputs: 13 | azureSubscription: $${{ parameters.serviceConnection }} 14 | scriptType: pscore 15 | scriptLocation: inlineScript 16 | addSpnToEnvironment: true 17 | inlineScript: | 18 | # Get settings from service connection 19 | az account show 2>$null | ConvertFrom-Json | Set-Variable account 20 | $clientId = $account.user.name 21 | $oidcToken = $env:idToken # requires addSpnToEnvironment: true 22 | $subscriptionId = $account.id 23 | $tenantId = $account.tenantId 24 | 25 | $env:ARM_TENANT_ID = $account.tenantId 26 | $env:ARM_SUBSCRIPTION_ID = $account.id 27 | $env:ARM_OIDC_TOKEN = $oidcToken 28 | $env:ARM_USE_OIDC = "true" 29 | $env:ARM_CLIENT_ID = $clientId 30 | $env:ARM_USE_AZUREAD = "true" 31 | 32 | # Run Terraform Plan 33 | $command = "terraform" 34 | $arguments = @() 35 | $arguments += "-chdir=$${{ parameters.root_module_folder_relative_path }}" 36 | $arguments += "plan" 37 | $arguments += "-out=tfplan" 38 | $arguments += "-input=false" 39 | 40 | if ($env:TERRAFORM_ACTION -eq 'destroy') { 41 | $arguments += "-destroy" 42 | } 43 | 44 | Write-Host "Running: $command $arguments" 45 | & $command $arguments 46 | 47 | env: 48 | TERRAFORM_ACTION: $${{ coalesce(parameters.terraform_action, 'apply') }} 49 | -------------------------------------------------------------------------------- /alz/azuredevops/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 4.20" 7 | } 8 | azapi = { 9 | source = "azure/azapi" 10 | version = "~> 2.2" 11 | } 12 | azuredevops = { 13 | source = "microsoft/azuredevops" 14 | version = "~> 1.7" 15 | } 16 | random = { 17 | source = "hashicorp/random" 18 | version = "~> 3.5" 19 | } 20 | http = { 21 | source = "hashicorp/http" 22 | version = "~> 3.4" 23 | } 24 | } 25 | } 26 | 27 | provider "azurerm" { 28 | subscription_id = var.bootstrap_subscription_id == "" ? null : var.bootstrap_subscription_id 29 | features { 30 | resource_group { 31 | prevent_deletion_if_contains_resources = false 32 | } 33 | storage { 34 | data_plane_available = false 35 | } 36 | } 37 | storage_use_azuread = true 38 | } 39 | 40 | provider "azuredevops" { 41 | personal_access_token = var.azure_devops_personal_access_token 42 | org_service_url = module.azure_devops.organization_url 43 | } 44 | -------------------------------------------------------------------------------- /alz/github/actions/bicep/main/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 02 Azure Landing Zones Continuous Delivery 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | skip_what_if: 10 | description: 'Skip What If Check?' 11 | default: false 12 | type: boolean 13 | %{ for script_file_group in script_file_groups ~} 14 | ${script_file_group.name}: 15 | description: 'Run Steps: ${script_file_group.displayName}' 16 | type: boolean 17 | default: true 18 | %{ endfor ~} 19 | destroy: 20 | description: '[DANGER!] Destroy? [DANGER!]' 21 | default: false 22 | type: boolean 23 | 24 | jobs: 25 | plan_and_apply: 26 | uses: ${organization_name}/${repository_name_templates}/${cd_template_path}@main 27 | name: 'CD' 28 | permissions: 29 | id-token: write 30 | contents: read 31 | with: 32 | skip_what_if: $${{ inputs.skip_what_if }} 33 | destroy: $${{ inputs.destroy }} 34 | %{ for script_file in script_files ~} 35 | ${script_file.name}: $${{ inputs.${script_file.group} }} 36 | %{ endfor ~} 37 | -------------------------------------------------------------------------------- /alz/github/actions/bicep/main/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 01 Azure Landing Zones Continuous Integration 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | validate_and_plan: 11 | uses: ${organization_name}/${repository_name_templates}/${ci_template_path}@main 12 | name: 'CI' 13 | permissions: 14 | id-token: write 15 | contents: read 16 | pull-requests: write 17 | -------------------------------------------------------------------------------- /alz/github/actions/bicep/templates/actions/bicep-destroy/action.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[DANGER!] DESTROY! [DANGER!]" 3 | description: "Destroy all the resources" 4 | inputs: 5 | whatIfEnabled: 6 | description: 'Is the WhatIf flag enabled?' 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - name: Run Bicep Deploy 13 | uses: azure/powershell@v2 14 | with: 15 | azPSVersion: 'latest' 16 | inlineScript: ./${destroy_script_path} -WhatIfEnabled $$${{ inputs.whatIfEnabled }} 17 | -------------------------------------------------------------------------------- /alz/github/actions/bicep/templates/actions/bicep-first-deployment-check/action.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: First Deployment Check 3 | description: Check to see if this is the first deployment 4 | inputs: 5 | managementGroupId: 6 | description: 'The management group id' 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - name: First Deployment Check 13 | id: firstDeployment 14 | uses: azure/powershell@v2 15 | with: 16 | azPSVersion: 'latest' 17 | inlineScript: | 18 | $managementGroupId = $env:MANAGEMENT_GROUP_ID 19 | $managementGroups = Get-AzManagementGroup 20 | $managementGroup = $managementGroups | Where-Object { $_.Name -eq $managementGroupId } 21 | 22 | $firstDeployment = $true 23 | 24 | if($managementGroup -eq $null) { 25 | Write-Warning "Cannot find the $managementGroupId Management Group, so assuming this is the first deployment. We must skip checking some deployments since their dependent resources do not exist yet." 26 | } else { 27 | Write-Host "Found the $managementGroupId Management Group, so assuming this is not the first deployment." 28 | $firstDeployment = $false 29 | } 30 | echo "firstDeployment=$firstDeployment" >> $env:GITHUB_ENV 31 | env: 32 | MANAGEMENT_GROUP_ID: $${{ inputs.managementGroupId }} 33 | -------------------------------------------------------------------------------- /alz/github/actions/bicep/templates/actions/bicep-installer/action.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Install Bicep and Update AZ PowerShell 3 | description: Install Bicep, Add to the Path and Update Az PowerShell to latest version 4 | 5 | runs: 6 | using: "composite" 7 | steps: 8 | - name: Check Az PowerShell Version 9 | run: | 10 | $latestVersion = (Find-Module AZ).Version 11 | Write-Host "Latest AZ PowerShell Version: $latestVersion" 12 | echo "lastestPowerShellAzVersion=$latestVersion" >> $env:GITHUB_ENV 13 | 14 | $installedModule = Get-InstalledModule -Name AZ -ErrorAction SilentlyContinue 15 | $installedVersion = $installedModule.Version 16 | Write-Host "Installed AZ PowerShell Version: $installedVersion" 17 | 18 | $runPowerShellAzUpgrade = $true 19 | 20 | if($installedVersion -ne $latestVersion) { 21 | Write-Host "Az PowerShell is not at the latest version, running upgrade step..." 22 | } else { 23 | Write-Host "Az PowerShell is already at the latest version, skipping upgrade step..." 24 | $runPowerShellAzUpgrade = $false 25 | } 26 | echo "runPowerShellAzUpgrade=$runPowerShellAzUpgrade" >> $env:GITHUB_ENV 27 | shell: pwsh 28 | 29 | - name: Upgrade AZ PowerShell 30 | uses: azure/powershell@v2 31 | if: $${{ env.runPowerShellAzUpgrade == 'true' }} 32 | with: 33 | azPSVersion: '$${{ env.lastestPowerShellAzVersion }}' 34 | inlineScript: | 35 | Write-Host "Attempted Upgrade of AZ PowerShell to $${{ env.lastestPowerShellAzVersion }}" 36 | 37 | - name: Install Bicep 38 | run: | 39 | $TOOLS_PATH = "$($env:GITHUB_WORKSPACE)/tools" 40 | $installDir = Join-Path -Path $TOOLS_PATH -ChildPath "bicep" 41 | 42 | $toolFileName = "bicep" 43 | $toolFilePath = Join-Path -Path $installDir -ChildPath $toolFileName 44 | 45 | if(!(Test-Path $installDir)) { 46 | New-Item -ItemType Directory -Path $installDir | Out-String | Write-Verbose 47 | } 48 | 49 | $url = "https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64" 50 | 51 | Invoke-WebRequest -Uri $url -OutFile "$toolFilePath" | Out-String | Write-Verbose 52 | 53 | $isExecutable = $(test -x $toolFilePath; 0 -eq $LASTEXITCODE) 54 | if(!($isExecutable)) { 55 | chmod +x $toolFilePath 56 | } 57 | 58 | $env:PATH = "$($installDir):$env:PATH" 59 | echo "$installDir" >> $env:GITHUB_PATH 60 | 61 | bicep --version 62 | 63 | Write-Host "Installed Latest Bicep Version" 64 | shell: pwsh 65 | -------------------------------------------------------------------------------- /alz/github/actions/bicep/templates/actions/bicep-on-demand-folder/action.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Get On Demand Folder 3 | description: Download and Copy On Demand Folder from Release Artifact 4 | inputs: 5 | repository: 6 | description: 'The source repository' 7 | required: true 8 | releaseArtifactName: 9 | description: 'The release artifact name' 10 | required: true 11 | releaseVersion: 12 | description: 'The release version' 13 | required: true 14 | sourcePath: 15 | description: 'The source path' 16 | required: true 17 | targetPath: 18 | description: 'The target path' 19 | required: true 20 | 21 | runs: 22 | using: "composite" 23 | steps: 24 | - name: Import Variables from File 25 | run: | 26 | $repository = "$${{ inputs.repository }}" 27 | $releaseArtifactName = "$${{ inputs.releaseArtifactName }}" 28 | $releaseVersion = "$${{ inputs.releaseVersion }}" 29 | $sourcePath = "$${{ inputs.sourcePath }}" 30 | $targetPath = "$${{ inputs.targetPath }}" 31 | 32 | Write-Host "Repository: $repository" 33 | Write-Host "Release Artifact Name: $releaseArtifactName" 34 | Write-Host "Release Version: $releaseVersion" 35 | Write-Host "Source Path: $sourcePath" 36 | Write-Host "Target Path: $targetPath" 37 | 38 | $repoOrgPlusRepo = $repository.Split("/")[-2..-1] -join "/" 39 | 40 | $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/tags/$releaseVersion" 41 | if($releaseVersion -eq "latest") { 42 | $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/latest" 43 | } 44 | $releaseData = Invoke-RestMethod $repoReleaseUrl -SkipHttpErrorCheck -StatusCodeVariable "statusCode" 45 | Write-Verbose "Status code: $statusCode" 46 | 47 | if($statusCode -eq 404) { 48 | Write-Error "The release $releaseVersion does not exist in the GitHub repository $repository - $repoReleaseUrl" 49 | throw "The release $releaseVersion does not exist in the GitHub repository $repository - $repoReleaseUrl" 50 | } 51 | 52 | # Handle transient errors like throttling 53 | if($statusCode -ge 400 -and $statusCode -le 599) { 54 | Write-Host "Retrying as got the Status Code $statusCode, which may be a tranisent error." -ForegroundColor Yellow 55 | $releaseData = Invoke-RestMethod $repoReleaseUrl -RetryIntervalSec 3 -MaximumRetryCount 100 56 | } 57 | 58 | if($statusCode -ne 200) { 59 | throw "Unable to query repository version, please check your internet connection and try again..." 60 | } 61 | 62 | $releaseArtifactUrl = $releaseData.assets | Where-Object { $_.name -eq $releaseArtifactName } | Select-Object -ExpandProperty browser_download_url 63 | 64 | $tempFolder = "./download" 65 | 66 | New-Item -ItemType Directory -Path $tempFolder | Out-String | Write-Verbose 67 | $targetPathForZip = "$tempFolder/$releaseArtifactName" 68 | Invoke-WebRequest -Uri $releaseArtifactUrl -OutFile $targetPathForZip -RetryIntervalSec 3 -MaximumRetryCount 100 | Out-String | Write-Verbose 69 | 70 | $targetPathForExtractedZip = "$tempFolder/extracted" 71 | 72 | Expand-Archive -Path $targetPathForZip -DestinationPath $targetPathForExtractedZip | Out-String | Write-Verbose 73 | 74 | $sourceFolderPath = "$($targetPathForExtractedZip)/$sourcePath/*" 75 | $targetFolderPath = "./$targetPath" 76 | 77 | Write-Host "Copying extracted files from $sourceFolderPath to $targetFolderPath" 78 | New-Item -ItemType Directory -Path $targetFolderPath | Out-String | Write-Verbose 79 | Copy-Item -Path $sourceFolderPath -Destination $targetFolderPath -Recurse -Force | Out-String | Write-Verbose 80 | 81 | Remove-Item -Path $tempFolder -Force -Recurse 82 | Write-Host "Successfully copied the files from the release artifact to the target path $targetPath..." 83 | 84 | shell: pwsh 85 | -------------------------------------------------------------------------------- /alz/github/actions/bicep/templates/actions/bicep-variables/action.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Import Variables from File 3 | description: Import variables from a JSON file 4 | inputs: 5 | parameters_file_name: 6 | description: 'The name of the file containing the parameters' 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - name: Import Variables from File 13 | run: | 14 | $fileName = $env:PARAMETERS_FILE_NAME 15 | Write-Host "Getting variables from $fileName" 16 | $json = Get-Content -Path $fileName | ConvertFrom-Json 17 | 18 | foreach ($key in $json.PSObject.Properties) { 19 | $envVarName = $key.Name 20 | $envVarValue = $key.Value 21 | echo "$envVarName=$envVarValue" >> $env:GITHUB_ENV 22 | echo "Set $envVarName to $envVarValue" 23 | } 24 | shell: pwsh 25 | env: 26 | PARAMETERS_FILE_NAME: $${{ inputs.parameters_file_name }} 27 | -------------------------------------------------------------------------------- /alz/github/actions/bicep/templates/workflows/ci-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous Integration 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | validate: 8 | name: Validate Bicep 9 | runs-on: 10 | ${runner_name} 11 | environment: ${environment_name_plan} 12 | permissions: 13 | id-token: write 14 | contents: read 15 | 16 | steps: 17 | - name: Checkout Bicep Module 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Bicep and Update Az Module 21 | uses: ${organization_name}/${repository_name_templates}/.github/actions/bicep-installer@main 22 | 23 | - name: Check for Custom Modules 24 | shell: pwsh 25 | working-directory: config 26 | run: | 27 | if (Test-Path -Path ./custom-modules/*) 28 | { 29 | echo "CUSTOM_MODULES=true" >> $env:GITHUB_ENV 30 | echo "Set CUSTOM_MODULES to true" 31 | } 32 | else 33 | { 34 | echo "Set CUSTOM_MODULES to false" 35 | } 36 | 37 | - name: Bicep Build & Lint All Custom Modules 38 | shell: pwsh 39 | if: $${{ env.CUSTOM_MODULES == 'true' }} 40 | working-directory: config/custom-modules 41 | run: | 42 | $output = @() 43 | Get-ChildItem -Recurse -Filter '*.bicep' | ForEach-Object { 44 | Write-Information "==> Attempting Bicep Build For File: $_" -InformationAction Continue 45 | $bicepOutput = bicep build $_.FullName 2>&1 46 | if ($LastExitCode -ne 0) 47 | { 48 | foreach ($item in $bicepOutput) { 49 | $output += "$($item) `r`n" 50 | } 51 | } 52 | else 53 | { 54 | echo "Bicep Build Successful for File: $_" 55 | } 56 | } 57 | if ($output.length -gt 0) { 58 | throw $output 59 | } 60 | 61 | whatif: 62 | name: What If 63 | runs-on: 64 | ${runner_name} 65 | concurrency: ${backend_azure_storage_account_container_name} 66 | environment: ${environment_name_plan} 67 | permissions: 68 | id-token: write 69 | contents: read 70 | env: 71 | PARAMETERS_FILE_NAME: parameters.json 72 | 73 | steps: 74 | - name: Checkout Bicep Module 75 | uses: actions/checkout@v4 76 | 77 | - name: Get Bicep Variables 78 | uses: ${organization_name}/${repository_name_templates}/.github/actions/bicep-variables@main 79 | with: 80 | parameters_file_name: $${{ env.PARAMETERS_FILE_NAME }} 81 | 82 | %{ for on_demand_folder in on_demand_folders ~} 83 | - name: Get On Demand Folder ${on_demand_folder.target} 84 | uses: ${organization_name}/${repository_name_templates}/.github/actions/bicep-on-demand-folder@main 85 | with: 86 | repository: "${on_demand_folder_repository}" 87 | releaseArtifactName: "${on_demand_folder_artifact_name}" 88 | releaseVersion: "$${{ env.RELEASE_VERSION }}" 89 | sourcePath: "${on_demand_folder.source}" 90 | targetPath: "${on_demand_folder.target}" 91 | 92 | %{ endfor ~} 93 | - name: Install Bicep and Update Az Module 94 | uses: ${organization_name}/${repository_name_templates}/.github/actions/bicep-installer@main 95 | 96 | - name: OIDC Login to Tenant 97 | uses: azure/login@v2 98 | with: 99 | client-id: $${{ vars.AZURE_CLIENT_ID }} 100 | tenant-id: $${{ vars.AZURE_TENANT_ID }} 101 | subscription-id: $${{ vars.AZURE_SUBSCRIPTION_ID }} 102 | enable-AzPSSession: true 103 | 104 | - name: First Deployment Check 105 | id: firstDeploymentCheck 106 | uses: ${organization_name}/${repository_name_templates}/.github/actions/bicep-first-deployment-check@main 107 | with: 108 | managementGroupId: $${{ env.MANAGEMENT_GROUP_ID }} 109 | %{ for script_file in script_files ~} 110 | - name: 'What If: ${script_file.displayName}' 111 | uses: ${organization_name}/${repository_name_templates}/.github/actions/bicep-deploy@main 112 | with: 113 | displayName: '${script_file.displayName}' 114 | templateFilePath: '${script_file.templateFilePath}' 115 | templateParametersFilePath: '${script_file.templateParametersFilePath}' 116 | managementGroupId: '${script_file.managementGroupIdVariable}' 117 | subscriptionId: '${script_file.subscriptionIdVariable}' 118 | resourceGroupName: '${script_file.resourceGroupNameVariable}' 119 | location: '$${{ env.LOCATION }}' 120 | deploymentType: '${script_file.deploymentType}' 121 | firstRunWhatIf: '${script_file.firstRunWhatIf}' 122 | firstDeployment: '$${{ env.firstDeployment }}' 123 | whatIfEnabled: 'true' 124 | %{ endfor ~} 125 | -------------------------------------------------------------------------------- /alz/github/actions/terraform/main/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 02 Azure Landing Zones Continuous Delivery 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | terraform_action: 10 | description: 'Terraform Action to perform' 11 | required: true 12 | default: 'apply' 13 | type: choice 14 | options: 15 | - 'apply' 16 | - 'destroy' 17 | terraform_cli_version: 18 | description: 'Terraform CLI Version' 19 | required: true 20 | default: 'latest' 21 | type: string 22 | 23 | jobs: 24 | plan_and_apply: 25 | uses: ${organization_name}/${repository_name_templates}/${cd_template_path}@main 26 | name: 'CD' 27 | permissions: 28 | id-token: write 29 | contents: read 30 | with: 31 | terraform_action: $${{ inputs.terraform_action }} 32 | root_module_folder_relative_path: '${root_module_folder_relative_path}' 33 | terraform_cli_version: $${{ inputs.terraform_cli_version }} 34 | -------------------------------------------------------------------------------- /alz/github/actions/terraform/main/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 01 Azure Landing Zones Continuous Integration 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | terraform_cli_version: 10 | description: 'Terraform CLI Version' 11 | required: true 12 | default: 'latest' 13 | type: string 14 | 15 | jobs: 16 | validate_and_plan: 17 | uses: ${organization_name}/${repository_name_templates}/${ci_template_path}@main 18 | name: 'CI' 19 | permissions: 20 | id-token: write 21 | contents: read 22 | pull-requests: write 23 | with: 24 | root_module_folder_relative_path: '${root_module_folder_relative_path}' 25 | terraform_cli_version: $${{ inputs.terraform_cli_version }} 26 | -------------------------------------------------------------------------------- /alz/github/actions/terraform/templates/workflows/cd-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous Delivery 3 | on: 4 | workflow_call: 5 | inputs: 6 | terraform_action: 7 | description: 'Terraform Action to perform' 8 | default: 'apply' 9 | type: string 10 | root_module_folder_relative_path: 11 | description: 'Root Module Folder Relative Path' 12 | default: '.' 13 | type: string 14 | terraform_cli_version: 15 | description: 'Terraform CLI Version' 16 | default: 'latest' 17 | type: string 18 | 19 | jobs: 20 | plan: 21 | name: Plan with Terraform 22 | runs-on: 23 | ${runner_name} 24 | concurrency: ${backend_azure_storage_account_container_name} 25 | environment: ${environment_name_plan} 26 | permissions: 27 | id-token: write 28 | contents: read 29 | env: 30 | ARM_CLIENT_ID: "$${{ vars.AZURE_CLIENT_ID }}" 31 | ARM_SUBSCRIPTION_ID: "$${{ vars.AZURE_SUBSCRIPTION_ID }}" 32 | ARM_TENANT_ID: "$${{ vars.AZURE_TENANT_ID }}" 33 | ARM_USE_AZUREAD: true 34 | ARM_USE_OIDC: true 35 | 36 | steps: 37 | - name: Checkout Code 38 | uses: actions/checkout@v4 39 | 40 | - name: Install Terraform 41 | uses: hashicorp/setup-terraform@v3 42 | with: 43 | terraform_wrapper: false 44 | terraform_version: $${{ inputs.terraform_cli_versions }} 45 | 46 | - name: Terraform Init 47 | run: | 48 | terraform \ 49 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 50 | init \ 51 | -backend-config="resource_group_name=$${{vars.BACKEND_AZURE_RESOURCE_GROUP_NAME}}" \ 52 | -backend-config="storage_account_name=$${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_NAME}}" \ 53 | -backend-config="container_name=$${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME}}" \ 54 | -backend-config="key=terraform.tfstate" 55 | 56 | - name: Terraform Plan for $${{ inputs.terraform_action == 'destroy' && 'Destroy' || 'Apply' }} 57 | run: | 58 | # shellcheck disable=SC2086 59 | terraform \ 60 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 61 | plan \ 62 | -out=tfplan \ 63 | -input=false \ 64 | $${{ inputs.terraform_action == 'destroy' && '-destroy' || '' }} 65 | 66 | - name: Create Module Artifact 67 | run: | 68 | $stagingDirectory = "staging" 69 | $rootModuleFolder = "$${{inputs.root_module_folder_relative_path}}" 70 | New-Item -Path . -Name $stagingDirectory -ItemType "directory" 71 | Copy-Item -Path "./*" -Exclude @(".git", ".terraform", ".github", $stagingDirectory) -Recurse -Destination "./$stagingDirectory" 72 | 73 | $rootModuleFolderTerraformFolder = Join-Path -Path "./$stagingDirectory" -ChildPath $rootModuleFolder -AdditionalChildPath ".terraform" 74 | if(Test-Path -Path $rootModuleFolderTerraformFolder) { 75 | Remove-Item -Path $rootModuleFolderTerraformFolder -Recurse -Force 76 | } 77 | 78 | shell: pwsh 79 | 80 | - name: Publish Module Artifact 81 | uses: actions/upload-artifact@v4 82 | with: 83 | name: module 84 | path: ./staging/ 85 | 86 | - name: Show the Plan for Review 87 | run: | 88 | terraform \ 89 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 90 | show \ 91 | tfplan 92 | 93 | apply: 94 | needs: plan 95 | name: Apply with Terraform 96 | runs-on: 97 | ${runner_name} 98 | concurrency: ${backend_azure_storage_account_container_name} 99 | environment: ${environment_name_apply} 100 | permissions: 101 | id-token: write 102 | contents: read 103 | env: 104 | ARM_CLIENT_ID: "$${{ vars.AZURE_CLIENT_ID }}" 105 | ARM_SUBSCRIPTION_ID: "$${{ vars.AZURE_SUBSCRIPTION_ID }}" 106 | ARM_TENANT_ID: "$${{ vars.AZURE_TENANT_ID }}" 107 | ARM_USE_AZUREAD: true 108 | ARM_USE_OIDC: true 109 | AZAPI_RETRY_GET_AFTER_PUT_MAX_TIME: "60m" # Accounts for eventually consistent management group permissions propagation 110 | 111 | steps: 112 | - name: Download a Build Artifact 113 | uses: actions/download-artifact@v4 114 | with: 115 | name: module 116 | 117 | - name: Install Terraform 118 | uses: hashicorp/setup-terraform@v3 119 | with: 120 | terraform_wrapper: false 121 | terraform_version: $${{ inputs.terraform_cli_versions }} 122 | 123 | - name: Terraform Init 124 | run: | 125 | terraform \ 126 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 127 | init \ 128 | -backend-config="resource_group_name=$${{vars.BACKEND_AZURE_RESOURCE_GROUP_NAME}}" \ 129 | -backend-config="storage_account_name=$${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_NAME}}" \ 130 | -backend-config="container_name=$${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME}}" \ 131 | -backend-config="key=terraform.tfstate" 132 | 133 | - name: Terraform $${{ inputs.terraform_action == 'destroy' && 'Destroy' || 'Apply' }} 134 | run: | 135 | terraform \ 136 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 137 | apply \ 138 | -input=false \ 139 | -auto-approve \ 140 | tfplan 141 | -------------------------------------------------------------------------------- /alz/github/actions/terraform/templates/workflows/ci-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous Integration 3 | on: 4 | workflow_call: 5 | inputs: 6 | root_module_folder_relative_path: 7 | description: 'Root Module Folder Relative Path' 8 | default: '.' 9 | type: string 10 | terraform_cli_version: 11 | description: 'Terraform CLI Version' 12 | default: 'latest' 13 | type: string 14 | 15 | jobs: 16 | validate: 17 | name: Validate Terraform 18 | runs-on: 19 | ${runner_name} 20 | environment: ${environment_name_plan} 21 | steps: 22 | - name: Checkout Code 23 | uses: actions/checkout@v4 24 | 25 | - name: Install Terraform 26 | uses: hashicorp/setup-terraform@v3 27 | with: 28 | terraform_wrapper: false 29 | terraform_version: $${{ inputs.terraform_cli_versions }} 30 | 31 | - name: Terraform Format Check 32 | run: | 33 | terraform \ 34 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 35 | fmt \ 36 | -check 37 | 38 | - name: Terraform Init 39 | run: | 40 | terraform \ 41 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 42 | init \ 43 | -backend=false 44 | 45 | - name: Terraform Validate 46 | run: | 47 | terraform \ 48 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 49 | validate 50 | 51 | plan: 52 | name: Validate Terraform Plan 53 | needs: validate 54 | runs-on: 55 | ${runner_name} 56 | concurrency: ${backend_azure_storage_account_container_name} 57 | environment: ${environment_name_plan} 58 | permissions: 59 | # NOTE: When modifying the token subject claims and adding `environment`. 60 | # If the `id-token` permission is granted at the workflow level 61 | # and the workflow has at least one job that does not specify an environment 62 | # then the action will fail with an internal error. 63 | id-token: write 64 | contents: read 65 | pull-requests: write 66 | env: 67 | ARM_CLIENT_ID: "$${{ vars.AZURE_CLIENT_ID }}" 68 | ARM_SUBSCRIPTION_ID: "$${{ vars.AZURE_SUBSCRIPTION_ID }}" 69 | ARM_TENANT_ID: "$${{ vars.AZURE_TENANT_ID }}" 70 | ARM_USE_AZUREAD: true 71 | ARM_USE_OIDC: true 72 | steps: 73 | - name: Checkout Code 74 | uses: actions/checkout@v4 75 | 76 | - name: Install Terraform 77 | uses: hashicorp/setup-terraform@v3 78 | with: 79 | terraform_wrapper: false 80 | terraform_version: $${{ inputs.terraform_cli_versions }} 81 | 82 | - name: Terraform Init 83 | run: | 84 | terraform \ 85 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 86 | init \ 87 | -backend-config="resource_group_name=$${{vars.BACKEND_AZURE_RESOURCE_GROUP_NAME}}" \ 88 | -backend-config="storage_account_name=$${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_NAME}}" \ 89 | -backend-config="container_name=$${{vars.BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME}}" \ 90 | -backend-config="key=terraform.tfstate" 91 | 92 | - name: Terraform Plan 93 | id: plan 94 | run: | 95 | terraform \ 96 | -chdir="$${{inputs.root_module_folder_relative_path}}" \ 97 | plan \ 98 | -input=false 99 | 100 | - name: Update Pull Request 101 | if: (success() || failure()) && github.event_name == 'pull_request' 102 | uses: actions/github-script@v6 103 | with: 104 | github-token: $${{ secrets.GITHUB_TOKEN }} 105 | script: | 106 | const output = `#### Terraform Plan 📖\`$${{ steps.plan.outcome }}\` 107 | 108 |
Run details 109 | 110 | The plan was a $${{ steps.plan.outcome }} see the action for more details. 111 | 112 |
113 | 114 | *Pushed by: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`*`; 115 | 116 | github.rest.issues.createComment({ 117 | issue_number: context.issue.number, 118 | owner: context.repo.owner, 119 | repo: context.repo.repo, 120 | body: output 121 | }) 122 | -------------------------------------------------------------------------------- /alz/github/data.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_client_config" "current" {} 2 | -------------------------------------------------------------------------------- /alz/github/locals.tf: -------------------------------------------------------------------------------- 1 | # Resource Name Setup 2 | locals { 3 | resource_names = module.resource_names.resource_names 4 | } 5 | 6 | locals { 7 | root_parent_management_group_id = var.root_parent_management_group_id == "" ? data.azurerm_client_config.current.tenant_id : var.root_parent_management_group_id 8 | } 9 | 10 | locals { 11 | enterprise_plan = "enterprise" 12 | } 13 | 14 | locals { 15 | iac_terraform = "terraform" 16 | } 17 | 18 | locals { 19 | use_private_networking = var.use_self_hosted_runners && var.use_private_networking 20 | allow_storage_access_from_my_ip = local.use_private_networking && var.allow_storage_access_from_my_ip 21 | } 22 | 23 | locals { 24 | use_runner_group = var.use_runner_group && module.github.organization_plan == local.enterprise_plan && var.use_self_hosted_runners 25 | runner_organization_repository_url = local.use_runner_group ? local.github_organization_url : "${local.github_organization_url}/${module.github.repository_names.module}" 26 | } 27 | 28 | locals { 29 | plan_key = "plan" 30 | apply_key = "apply" 31 | } 32 | 33 | locals { 34 | ci_template_file_name = "workflows/ci-template.yaml" 35 | cd_template_file_name = "workflows/cd-template.yaml" 36 | } 37 | 38 | locals { 39 | target_subscriptions = distinct([var.subscription_id_connectivity, var.subscription_id_identity, var.subscription_id_management]) 40 | } 41 | 42 | locals { 43 | environments = { 44 | (local.plan_key) = local.resource_names.version_control_system_environment_plan 45 | (local.apply_key) = local.resource_names.version_control_system_environment_apply 46 | } 47 | } 48 | 49 | locals { 50 | managed_identities = { 51 | (local.plan_key) = local.resource_names.user_assigned_managed_identity_plan 52 | (local.apply_key) = local.resource_names.user_assigned_managed_identity_apply 53 | } 54 | 55 | federated_credentials = { for key, value in module.github.subjects : 56 | key => { 57 | user_assigned_managed_identity_key = value.user_assigned_managed_identity_key 58 | federated_credential_subject = value.subject 59 | federated_credential_issuer = module.github.issuer 60 | federated_credential_name = "${local.resource_names.user_assigned_managed_identity_federated_credentials_prefix}-${key}" 61 | } 62 | } 63 | 64 | runner_container_instances = var.use_self_hosted_runners ? { 65 | agent_01 = { 66 | container_instance_name = local.resource_names.container_instance_01 67 | agent_name = local.resource_names.runner_01 68 | cpu = var.runner_container_cpu 69 | memory = var.runner_container_memory 70 | cpu_max = var.runner_container_cpu_max 71 | memory_max = var.runner_container_memory_max 72 | zones = var.runner_container_zone_support ? ["1"] : [] 73 | } 74 | agent_02 = { 75 | container_instance_name = local.resource_names.container_instance_02 76 | agent_name = local.resource_names.runner_02 77 | cpu = var.runner_container_cpu 78 | memory = var.runner_container_memory 79 | cpu_max = var.runner_container_cpu_max 80 | memory_max = var.runner_container_memory_max 81 | zones = var.runner_container_zone_support ? ["2"] : [] 82 | } 83 | } : {} 84 | } 85 | 86 | locals { 87 | starter_module_folder_path = var.module_folder_path_relative ? ("${path.module}/${var.module_folder_path}") : var.module_folder_path 88 | starter_root_module_folder_path = "${local.starter_module_folder_path}/${var.root_module_folder_relative_path}" 89 | } 90 | 91 | locals { 92 | runner_container_instance_dockerfile_url = "${var.runner_container_image_repository}#${var.runner_container_image_tag}:${var.runner_container_image_folder}" 93 | } 94 | 95 | locals { 96 | custom_role_definitions_bicep_names = { for key, value in var.custom_role_definitions_bicep : "custom_role_definition_bicep_${key}" => value.name } 97 | custom_role_definitions_terraform_names = { for key, value in var.custom_role_definitions_terraform : "custom_role_definition_terraform_${key}" => value.name } 98 | 99 | custom_role_definitions_bicep = { 100 | for key, value in var.custom_role_definitions_bicep : key => { 101 | name = local.resource_names["custom_role_definition_bicep_${key}"] 102 | description = value.description 103 | permissions = value.permissions 104 | } 105 | } 106 | 107 | custom_role_definitions_terraform = { 108 | for key, value in var.custom_role_definitions_terraform : key => { 109 | name = local.resource_names["custom_role_definition_terraform_${key}"] 110 | description = value.description 111 | permissions = value.permissions 112 | } 113 | } 114 | } 115 | 116 | locals { 117 | architecture_definition_name = var.architecture_definition_name 118 | has_architecture_definition = var.architecture_definition_name != null && var.architecture_definition_name != "" 119 | } 120 | 121 | locals { 122 | github_organization_url = "${var.github_organization_scheme}://${var.github_organization_domain_name}/${var.github_organization_name}" 123 | github_api_base_url = var.github_api_domain_name == "" ? "${var.github_organization_scheme}://api.${var.github_organization_domain_name}/" : "${var.github_organization_scheme}://${var.github_api_domain_name}/" 124 | } 125 | -------------------------------------------------------------------------------- /alz/github/locals.workflows.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | workflows = { 3 | ci = { 4 | workflow_file_name = "${local.target_folder_name}/${local.ci_template_file_name}" 5 | environment_user_assigned_managed_identity_mappings = [{ 6 | environment_key = local.plan_key 7 | user_assigned_managed_identity_key = local.plan_key 8 | }] 9 | } 10 | cd = { 11 | workflow_file_name = "${local.target_folder_name}/${local.cd_template_file_name}" 12 | environment_user_assigned_managed_identity_mappings = [{ 13 | environment_key = local.plan_key 14 | user_assigned_managed_identity_key = local.plan_key 15 | }, 16 | { 17 | environment_key = local.apply_key 18 | user_assigned_managed_identity_key = local.apply_key 19 | }] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /alz/github/outputs.tf: -------------------------------------------------------------------------------- 1 | output "details" { 2 | description = "The details of the settings used" 3 | value = { 4 | iac_type = var.iac_type 5 | starter_module_name = var.starter_module_name 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /alz/github/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 4.20" 7 | } 8 | azapi = { 9 | source = "azure/azapi" 10 | version = "~> 2.2" 11 | } 12 | github = { 13 | source = "integrations/github" 14 | version = "~> 6.5" 15 | } 16 | random = { 17 | source = "hashicorp/random" 18 | version = "~> 3.5" 19 | } 20 | http = { 21 | source = "hashicorp/http" 22 | version = "~> 3.4" 23 | } 24 | } 25 | } 26 | 27 | provider "azurerm" { 28 | subscription_id = var.bootstrap_subscription_id == "" ? null : var.bootstrap_subscription_id 29 | features { 30 | resource_group { 31 | prevent_deletion_if_contains_resources = false 32 | } 33 | storage { 34 | data_plane_available = false 35 | } 36 | } 37 | storage_use_azuread = true 38 | } 39 | 40 | provider "github" { 41 | token = var.github_personal_access_token 42 | owner = var.github_organization_name 43 | base_url = local.github_api_base_url 44 | } 45 | -------------------------------------------------------------------------------- /alz/local/data.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_client_config" "current" {} 2 | -------------------------------------------------------------------------------- /alz/local/locals.files.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | deploy_script_file_directory_path = "${path.module}/scripts" 3 | 4 | deploy_script_files = var.iac_type == "bicep" ? fileset(local.deploy_script_file_directory_path, "**/*.ps1") : [] 5 | 6 | starter_module_config = var.iac_type == "bicep" ? jsondecode(file("${var.module_folder_path}/${var.bicep_config_file_path}")).starter_modules[var.starter_module_name] : null 7 | script_files_all = var.iac_type == "bicep" ? local.starter_module_config.deployment_files : [] 8 | 9 | target_folder_name = "scripts" 10 | 11 | # Get a list of on-demand folders 12 | on_demand_folders = var.iac_type == "bicep" ? local.starter_module_config.on_demand_folders : [] 13 | 14 | networking_type = var.iac_type == "bicep" ? jsondecode(file("${var.module_folder_path}/${var.bicep_parameters_file_path}")).NETWORK_TYPE : "" 15 | script_files = var.iac_type == "bicep" ? { for script_file in local.script_files_all : format("%03d", script_file.order) => { 16 | name = script_file.name 17 | displayName = script_file.displayName 18 | templateFilePath = script_file.templateFilePath 19 | templateParametersFilePath = script_file.templateParametersFilePath 20 | managementGroupIdVariable = try("$env:${script_file.managementGroupId}", "\"\"") 21 | subscriptionIdVariable = try("$env:${script_file.subscriptionId}", "\"\"") 22 | resourceGroupNameVariable = try("$env:${script_file.resourceGroupName}", "\"\"") 23 | deploymentType = script_file.deploymentType 24 | firstRunWhatIf = format("%s%s", "$", script_file.firstRunWhatIf) 25 | group = script_file.group 26 | } if try(script_file.networkType, "") == "" || try(script_file.networkType, "") == local.networking_type } : {} 27 | 28 | deploy_script_files_parsed = { for deploy_script_file in local.deploy_script_files : "${local.target_folder_name}/${deploy_script_file}" => 29 | { 30 | content = templatefile("${local.deploy_script_file_directory_path}/${deploy_script_file}", { 31 | script_files = local.script_files 32 | on_demand_folders = local.on_demand_folders 33 | on_demand_folder_repository = var.on_demand_folder_repository 34 | on_demand_folder_artifact_name = var.on_demand_folder_artifact_name 35 | }) 36 | } 37 | } 38 | 39 | module_files = { for key, value in module.files.files : key => 40 | { 41 | content = try(replace((file(value.path)), "# backend \"azurerm\" {}", (var.create_bootstrap_resources_in_azure ? "backend \"azurerm\" {}" : "backend \"local\" {}")), "unsupported_file_type") 42 | } if var.iac_type == "bicep" ? true : !endswith(key, ".ps1") 43 | } 44 | 45 | # Build a map of module files with types that are supported 46 | module_files_supported = { for key, value in local.module_files : key => value if value.content != "unsupported_file_type" && !endswith(key, "-cache.json") && !endswith(key, var.bicep_config_file_path) } 47 | 48 | # Build a list of files to exclude from the repository based on the on-demand folders 49 | excluded_module_files = distinct(flatten([for exclusion in local.on_demand_folders : 50 | [for key, value in local.module_files_supported : key if startswith(key, exclusion.target)] 51 | ])) 52 | 53 | # Filter out the excluded files 54 | module_files_filtered = { for key, value in local.module_files_supported : key => value if !contains(local.excluded_module_files, key) } 55 | 56 | final_module_files = merge(local.module_files_filtered, local.deploy_script_files_parsed) 57 | } 58 | -------------------------------------------------------------------------------- /alz/local/locals.terraform.script.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | command_replacements = { 3 | root_module_folder_relative_path = var.root_module_folder_relative_path 4 | remote_state_resource_group_name = var.create_bootstrap_resources_in_azure ? local.resource_names.resource_group_state : "" 5 | remote_state_storage_account_name = var.create_bootstrap_resources_in_azure ? local.resource_names.storage_account : "" 6 | remote_state_storage_container_name = var.create_bootstrap_resources_in_azure ? local.resource_names.storage_container : "" 7 | } 8 | 9 | command_final = templatefile("${path.module}/templates/terraform-deploy-local.ps1", local.command_replacements) 10 | } 11 | -------------------------------------------------------------------------------- /alz/local/locals.tf: -------------------------------------------------------------------------------- 1 | # Resource Name Setup 2 | locals { 3 | resource_names = module.resource_names.resource_names 4 | } 5 | 6 | locals { 7 | root_parent_management_group_id = var.root_parent_management_group_id == "" ? data.azurerm_client_config.current.tenant_id : var.root_parent_management_group_id 8 | } 9 | 10 | locals { 11 | iac_terraform = "terraform" 12 | } 13 | 14 | locals { 15 | plan_key = "plan" 16 | apply_key = "apply" 17 | } 18 | 19 | locals { 20 | target_subscriptions = distinct([var.subscription_id_connectivity, var.subscription_id_identity, var.subscription_id_management]) 21 | } 22 | 23 | locals { 24 | managed_identities = { 25 | (local.plan_key) = local.resource_names.user_assigned_managed_identity_plan 26 | (local.apply_key) = local.resource_names.user_assigned_managed_identity_apply 27 | } 28 | 29 | federated_credentials = var.federated_credentials 30 | } 31 | 32 | locals { 33 | starter_module_folder_path = var.module_folder_path_relative ? ("${path.module}/${var.module_folder_path}") : var.module_folder_path 34 | starter_root_module_folder_path = "${local.starter_module_folder_path}/${var.root_module_folder_relative_path}" 35 | } 36 | 37 | locals { 38 | target_directory = var.target_directory == "" ? ("${path.module}/${var.default_target_directory}") : var.target_directory 39 | } 40 | 41 | locals { 42 | custom_role_definitions_bicep_names = { for key, value in var.custom_role_definitions_bicep : "custom_role_definition_bicep_${key}" => value.name } 43 | custom_role_definitions_terraform_names = { for key, value in var.custom_role_definitions_terraform : "custom_role_definition_terraform_${key}" => value.name } 44 | 45 | custom_role_definitions_bicep = { 46 | for key, value in var.custom_role_definitions_bicep : key => { 47 | name = local.resource_names["custom_role_definition_bicep_${key}"] 48 | description = value.description 49 | permissions = value.permissions 50 | } 51 | } 52 | 53 | custom_role_definitions_terraform = { 54 | for key, value in var.custom_role_definitions_terraform : key => { 55 | name = local.resource_names["custom_role_definition_terraform_${key}"] 56 | description = value.description 57 | permissions = value.permissions 58 | } 59 | } 60 | } 61 | 62 | locals { 63 | architecture_definition_name = var.architecture_definition_name 64 | has_architecture_definition = var.architecture_definition_name != null && var.architecture_definition_name != "" 65 | architecture_definition_file_destination = var.architecture_definition_name != null && var.architecture_definition_name != "" ? "${local.target_directory}/${var.root_module_folder_relative_path}/lib/architecture_definitions/${local.architecture_definition_name}.alz_architecture_definition.json" : "" 66 | } 67 | -------------------------------------------------------------------------------- /alz/local/main.tf: -------------------------------------------------------------------------------- 1 | module "resource_names" { 2 | source = "../../modules/resource_names" 3 | azure_location = var.bootstrap_location 4 | environment_name = var.environment_name 5 | service_name = var.service_name 6 | postfix_number = var.postfix_number 7 | resource_names = merge(var.resource_names, local.custom_role_definitions_bicep_names, local.custom_role_definitions_terraform_names) 8 | } 9 | 10 | module "architecture_definition" { 11 | count = local.has_architecture_definition ? 1 : 0 12 | source = "../../modules/template_architecture_definition" 13 | starter_module_folder_path = local.starter_root_module_folder_path 14 | architecture_definition_name = local.architecture_definition_name 15 | architecture_definition_template_path = var.architecture_definition_template_path 16 | architecture_definition_override_path = var.architecture_definition_override_path 17 | apply_alz_archetypes_via_architecture_definition_template = var.apply_alz_archetypes_via_architecture_definition_template 18 | } 19 | 20 | resource "local_file" "architecture_definition_file" { 21 | count = local.has_architecture_definition ? 1 : 0 22 | content = module.architecture_definition[0].architecture_definition_json 23 | filename = local.architecture_definition_file_destination 24 | } 25 | 26 | module "files" { 27 | source = "../../modules/files" 28 | starter_module_folder_path = local.starter_module_folder_path 29 | additional_files = var.additional_files 30 | configuration_file_path = var.configuration_file_path 31 | built_in_configuration_file_names = var.built_in_configuration_file_names 32 | additional_folders_path = var.additional_folders_path 33 | } 34 | 35 | module "azure" { 36 | source = "../../modules/azure" 37 | count = var.create_bootstrap_resources_in_azure ? 1 : 0 38 | user_assigned_managed_identities = local.managed_identities 39 | federated_credentials = local.federated_credentials 40 | resource_group_identity_name = local.resource_names.resource_group_identity 41 | resource_group_state_name = local.resource_names.resource_group_state 42 | create_storage_account = var.iac_type == local.iac_terraform 43 | storage_account_name = local.resource_names.storage_account 44 | storage_container_name = local.resource_names.storage_container 45 | azure_location = var.bootstrap_location 46 | target_subscriptions = local.target_subscriptions 47 | root_parent_management_group_id = local.root_parent_management_group_id 48 | storage_account_replication_type = var.storage_account_replication_type 49 | use_self_hosted_agents = false 50 | use_private_networking = false 51 | custom_role_definitions = var.iac_type == "terraform" ? local.custom_role_definitions_terraform : local.custom_role_definitions_bicep 52 | role_assignments = var.iac_type == "terraform" ? var.role_assignments_terraform : var.role_assignments_bicep 53 | additional_role_assignment_principal_ids = var.grant_permissions_to_current_user ? { current_user = data.azurerm_client_config.current.object_id } : {} 54 | } 55 | 56 | resource "local_file" "alz" { 57 | for_each = local.final_module_files 58 | content = each.value.content 59 | filename = "${local.target_directory}/${each.key}" 60 | } 61 | 62 | resource "local_file" "command" { 63 | count = var.iac_type == "terraform" ? 1 : 0 64 | content = local.command_final 65 | filename = "${local.target_directory}/scripts/deploy-local.ps1" 66 | } 67 | -------------------------------------------------------------------------------- /alz/local/outputs.tf: -------------------------------------------------------------------------------- 1 | output "module_output_directory_path" { 2 | description = "The path to the directory where the module files have been created." 3 | value = abspath(local.target_directory) 4 | } 5 | 6 | output "details" { 7 | description = "The details of the settings used" 8 | value = { 9 | iac_type = var.iac_type 10 | starter_module_name = var.starter_module_name 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /alz/local/scripts/bicep-deploy.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [switch]$whatIf, 3 | [string]$displayName, 4 | [string]$templateFilePath, 5 | [string]$templateParametersFilePath, 6 | [string]$managementGroupId, 7 | [string]$subscriptionId, 8 | [string]$resourceGroupName, 9 | [string]$location, 10 | [string]$deploymentType, 11 | [bool]$firstRunWhatIf, 12 | [bool]$firstDeployment 13 | ) 14 | 15 | Write-Host "<---------------------------------------------------------------------------->" -ForegroundColor Blue 16 | Write-Host "Starting $($whatIf ? "What If" : "Full") $displayName..." -ForegroundColor Blue 17 | Write-Host "<---------------------------------------------------------------------------->" -ForegroundColor Blue 18 | Write-Host "" 19 | 20 | Write-Host "What If: $whatIf" -ForegroundColor DarkGray 21 | Write-Host "Display Name: $displayName" -ForegroundColor DarkGray 22 | Write-Host "Template File Path: $templateFilePath" -ForegroundColor DarkGray 23 | Write-Host "Template Parameters File Path: $templateParametersFilePath" -ForegroundColor DarkGray 24 | Write-Host "Management Group Id: $managementGroupId" -ForegroundColor DarkGray 25 | Write-Host "Subscription Id: $subscriptionId" -ForegroundColor DarkGray 26 | Write-Host "Resource Group Name: $resourceGroupName" -ForegroundColor DarkGray 27 | Write-Host "Location: $location" -ForegroundColor DarkGray 28 | Write-Host "Deployment Type: $deploymentType" -ForegroundColor DarkGray 29 | Write-Host "First Run What If: $firstRunWhatIf" -ForegroundColor DarkGray 30 | Write-Host "First Deployment: $firstDeployment" -ForegroundColor DarkGray 31 | 32 | if($whatIf -and $firstDeployment -and !$firstRunWhatIf) { 33 | Write-Host "Skipping the WhatIf check as the deployment is dependent on resources that do not exist yet..." -ForegroundColor Cyan 34 | return 35 | } 36 | 37 | $deploymentPrefix = $env:PREFIX 38 | $deploymentName = $displayName.Replace(" ", "-") 39 | $deploymentTimeStamp = Get-Date -Format 'yyyyMMddHHmmss' 40 | 41 | $prefixPostFixAndHythenLength = $deploymentPrefix.Length + $deploymentTimeStamp.Length + 2 42 | $deploymentNameMaxLength = 61 - $prefixPostFixAndHythenLength 43 | 44 | if($deploymentName.Length -gt $deploymentNameMaxLength) { 45 | $deploymentName = $deploymentName.Substring(0, $deploymentNameMaxLength) 46 | } 47 | 48 | $deploymentName = "$deploymentPrefix-$deploymentName-$deploymentTimeStamp" 49 | Write-Host "Deployment Name: $deploymentName" 50 | 51 | $inputObject = @{ 52 | TemplateFile = $templateFilePath 53 | TemplateParameterFile = $templateParametersFilePath 54 | WhatIf = $whatIf 55 | } 56 | 57 | $retryCount = 0 58 | $retryMax = 30 59 | $initialRetryDelay = 20 60 | $retryDelayIncrement = 10 61 | $finalSuccess = $false 62 | 63 | while ($retryCount -lt $retryMax) { 64 | $retryAttempt = '{0:d2}' -f ($retryCount + 1) 65 | 66 | if($retryCount -gt 0) { 67 | $retryDelay = $initialRetryDelay + ($retryCount * $retryDelayIncrement) 68 | Write-Host "Retrying deployment with attempt number $retryAttempt after $retryDelay seconds..." -ForegroundColor Green 69 | Start-Sleep -Seconds $retryDelay 70 | Write-Host "Retrying deployment..." -ForegroundColor Green 71 | } 72 | 73 | $inputObject.DeploymentName = "$deploymentName-$retryAttempt" 74 | 75 | $result = $null 76 | 77 | try { 78 | if ($deploymentType -eq "tenant") { 79 | $inputObject.Location = $location 80 | $result = New-AzTenantDeployment @inputObject 81 | } 82 | 83 | if ($deploymentType -eq "managementGroup") { 84 | $inputObject.Location = $location 85 | $inputObject.ManagementGroupId = $managementGroupId 86 | if ($inputObject.ManagementGroupId -eq "") { 87 | $inputObject.ManagementGroupId = (Get-AzContext).Tenant.TenantId 88 | } 89 | $result = New-AzManagementGroupDeployment @inputObject 90 | } 91 | 92 | if ($deploymentType -eq "subscription") { 93 | $inputObject.Location = $location 94 | Select-AzSubscription -SubscriptionId $subscriptionId 95 | $result = New-AzSubscriptionDeployment @inputObject 96 | } 97 | 98 | if ($deploymentType -eq "resourceGroup") { 99 | $inputObject.ResourceGroupName = $resourceGroupName 100 | Select-AzSubscription -SubscriptionId $subscriptionId 101 | $result = New-AzResourceGroupDeployment @inputObject 102 | } 103 | } catch { 104 | Write-Host $_ -ForegroundColor Red 105 | Write-Host "Deployment failed with exception, this is likely an intermittent failure so entering retry loop..." -ForegroundColor Red 106 | $retryCount++ 107 | continue 108 | } 109 | 110 | if ($whatIf) { 111 | $result | Format-List | Out-Host 112 | return 113 | } 114 | 115 | $resultId = "" 116 | 117 | if($deploymentType -eq "resourceGroup") { 118 | $resultId = "/subscriptions/$($subscriptionId)/resourceGroups/$($resourceGroupName)/providers/Microsoft.Resources/deployments/$deploymentName" 119 | } else { 120 | $resultId = $result.Id 121 | } 122 | 123 | $resultIdEscaped = $resultId.Replace("/", "%2F") 124 | $resultUrl = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/$resultIdEscaped" 125 | 126 | Write-Host "Deployment Name: $deploymentName" 127 | Write-Host "Deployment ID: $resultId" 128 | Write-Host "Deployment Url: $resultUrl" 129 | $result | Format-List | Out-Host 130 | 131 | if($result.ProvisioningState -ne "Succeeded") { 132 | Write-Host "Deployment failed with unsuccessful provisioning state, this is likely an intermittent failure so entering retry loop..." -ForegroundColor Red 133 | $retryCount++ 134 | } else { 135 | $finalSuccess = $true 136 | break 137 | } 138 | } 139 | 140 | if($finalSuccess -eq $false) { 141 | Write-Error "Deployment failed after $retryMax attempts..." 142 | } 143 | 144 | Write-Host "<---------------------------------------------------------------------------->" -ForegroundColor DarkMagenta 145 | Write-Host "Completed $($whatIf ? "What If" : "Full") $displayName..." -ForegroundColor DarkMagenta 146 | Write-Host "<---------------------------------------------------------------------------->" -ForegroundColor DarkMagenta 147 | Write-Host "" -------------------------------------------------------------------------------- /alz/local/scripts/bicep-first-deployment-check.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$managementGroupId 3 | ) 4 | 5 | $managementGroups = Get-AzManagementGroup 6 | $managementGroup = $managementGroups | Where-Object { $_.Name -eq $managementGroupId } 7 | 8 | $firstDeployment = $true 9 | 10 | if($null -eq $managementGroup) { 11 | Write-Warning "Cannot find the $managementGroupId Management Group, so assuming this is the first deployment. We must skip checking some deployments since their dependent resources do not exist yet." 12 | } else { 13 | Write-Host "Found the $managementGroupId Management Group, so assuming this is not the first deployment." 14 | $firstDeployment = $false 15 | } 16 | 17 | return $firstDeployment -------------------------------------------------------------------------------- /alz/local/scripts/bicep-get-variables.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$fileName="parameters.json" 3 | ) 4 | 5 | Write-Host "Getting variables from $fileName" 6 | $json = Get-Content -Path $fileName | ConvertFrom-Json 7 | 8 | foreach ($key in $json.PSObject.Properties) { 9 | $envVarName = $key.Name 10 | $envVarValue = $key.Value 11 | [Environment]::SetEnvironmentVariable($envVarName, $envVarValue) 12 | Write-Output "Set $envVarName to $envVarValue" 13 | } -------------------------------------------------------------------------------- /alz/local/scripts/bicep-on-demand-folder.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$repository, 3 | [string]$releaseArtifactName, 4 | [string]$releaseVersion, 5 | [string]$sourcePath, 6 | [string]$targetPath 7 | ) 8 | 9 | Write-Host "Repository: $repository" 10 | Write-Host "Release Artifact Name: $releaseArtifactName" 11 | Write-Host "Release Version: $releaseVersion" 12 | Write-Host "Source Path: $sourcePath" 13 | Write-Host "Target Path: $targetPath" 14 | 15 | $targetFolderPath = "./$targetPath" 16 | 17 | if(Test-Path $targetFolderPath) { 18 | Write-Host "Deleting the existing target path $targetFolderPath..." 19 | Remove-Item -Path $targetFolderPath -Force -Recurse 20 | } 21 | 22 | $repoOrgPlusRepo = $repository.Split("/")[-2..-1] -join "/" 23 | 24 | $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/tags/$releaseVersion" 25 | if($releaseVersion -eq "latest") { 26 | $repoReleaseUrl = "https://api.github.com/repos/$repoOrgPlusRepo/releases/latest" 27 | } 28 | $releaseData = Invoke-RestMethod $repoReleaseUrl -SkipHttpErrorCheck -StatusCodeVariable "statusCode" 29 | Write-Verbose "Status code: $statusCode" 30 | 31 | if($statusCode -eq 404) { 32 | Write-Error "The release $releaseVersion does not exist in the GitHub repository $repository - $repoReleaseUrl" 33 | throw "The release $releaseVersion does not exist in the GitHub repository $repository - $repoReleaseUrl" 34 | } 35 | 36 | # Handle transient errors like throttling 37 | if($statusCode -ge 400 -and $statusCode -le 599) { 38 | Write-Host "Retrying as got the Status Code $statusCode, which may be a tranisent error." -ForegroundColor Yellow 39 | $releaseData = Invoke-RestMethod $repoReleaseUrl -RetryIntervalSec 3 -MaximumRetryCount 100 40 | } 41 | 42 | if($statusCode -ne 200) { 43 | throw "Unable to query repository version, please check your internet connection and try again..." 44 | } 45 | 46 | $releaseArtifactUrl = $releaseData.assets | Where-Object { $_.name -eq $releaseArtifactName } | Select-Object -ExpandProperty browser_download_url 47 | 48 | $tempFolder = "./download" 49 | 50 | New-Item -ItemType Directory -Path $tempFolder | Out-String | Write-Verbose 51 | $targetPathForZip = "$tempFolder/$releaseArtifactName" 52 | Invoke-WebRequest -Uri $releaseArtifactUrl -OutFile $targetPathForZip -RetryIntervalSec 3 -MaximumRetryCount 100 | Out-String | Write-Verbose 53 | 54 | $targetPathForExtractedZip = "$tempFolder/extracted" 55 | 56 | Expand-Archive -Path $targetPathForZip -DestinationPath $targetPathForExtractedZip | Out-String | Write-Verbose 57 | 58 | $sourceFolderPath = "$($targetPathForExtractedZip)/$sourcePath/*" 59 | 60 | Write-Host "Copying extracted files from $sourceFolderPath to $targetFolderPath" 61 | New-Item -ItemType Directory -Path $targetFolderPath | Out-String | Write-Verbose 62 | Copy-Item -Path $sourceFolderPath -Destination $targetFolderPath -Recurse -Force | Out-String | Write-Verbose 63 | 64 | Remove-Item -Path $tempFolder -Force -Recurse 65 | Write-Host "Successfully copied the files from the release artifact to the target path $targetPath..." 66 | -------------------------------------------------------------------------------- /alz/local/scripts/deploy-local.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param() 3 | 4 | $verbose = $PSBoundParameters.Verbose.IsPresent 5 | Write-Host "Verbose: $verbose" 6 | 7 | # Getting the variables from the parameters.json file 8 | ./scripts/bicep-get-variables.ps1 -fileName "parameters.json" 9 | 10 | # Checking if this is the first deployment 11 | $isFirstDeployment = ./scripts/bicep-first-deployment-check.ps1 -managementGroupId $env:MANAGEMENT_GROUP_ID 12 | 13 | %{ for on_demand_folder in on_demand_folders ~} 14 | # Downloading the on demand folder for ${on_demand_folder.target} 15 | ./scripts/bicep-on-demand-folder.ps1 ` 16 | -repository "${on_demand_folder_repository}" ` 17 | -releaseArtifactName "${on_demand_folder_artifact_name}" ` 18 | -releaseVersion $env:RELEASE_VERSION ` 19 | -sourcePath "${on_demand_folder.source}" ` 20 | -targetPath "${on_demand_folder.target}" ` 21 | %{ endfor ~} 22 | 23 | %{ for script_file in script_files ~} 24 | # Running What If for ${script_file.displayName} 25 | ./scripts/bicep-deploy.ps1 ` 26 | -displayName "${script_file.displayName}" ` 27 | -templateFilePath "${script_file.templateFilePath}" ` 28 | -templateParametersFilePath "${script_file.templateParametersFilePath}" ` 29 | -managementGroupId ${script_file.managementGroupIdVariable} ` 30 | -subscriptionId ${script_file.subscriptionIdVariable} ` 31 | -resourceGroupName ${script_file.resourceGroupNameVariable} ` 32 | -location $env:LOCATION ` 33 | -deploymentType "${script_file.deploymentType}" ` 34 | -firstRunWhatIf ${script_file.firstRunWhatIf} ` 35 | -firstDeployment $isFirstDeployment ` 36 | -whatIf 37 | 38 | %{ endfor ~} 39 | 40 | Write-Host "" 41 | $deployApproved = Read-Host -Prompt "Type 'yes' and hit Enter to continue with the full deployment" 42 | Write-Host "" 43 | 44 | if($deployApproved -ne "yes") { 45 | Write-Error "Deployment was not approved. Exiting..." 46 | exit 1 47 | } 48 | 49 | %{ for script_file in script_files ~} 50 | # Running Deployment for ${script_file.displayName} 51 | ./scripts/bicep-deploy.ps1 ` 52 | -displayName "${script_file.displayName}" ` 53 | -templateFilePath "${script_file.templateFilePath}" ` 54 | -templateParametersFilePath "${script_file.templateParametersFilePath}" ` 55 | -managementGroupId ${script_file.managementGroupIdVariable} ` 56 | -subscriptionId ${script_file.subscriptionIdVariable} ` 57 | -resourceGroupName ${script_file.resourceGroupNameVariable} ` 58 | -location $env:LOCATION ` 59 | -deploymentType "${script_file.deploymentType}" ` 60 | -firstRunWhatIf ${script_file.firstRunWhatIf} ` 61 | -firstDeployment $isFirstDeployment ` 62 | 63 | %{ endfor ~} 64 | -------------------------------------------------------------------------------- /alz/local/templates/terraform-deploy-local.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [switch]$destroy, 3 | [string]$root_module_folder_relative_path = "${root_module_folder_relative_path}", 4 | [string]$remote_state_resource_group_name = "${remote_state_resource_group_name}", 5 | [string]$remote_state_storage_account_name = "${remote_state_storage_account_name}", 6 | [string]$remote_state_storage_container_name = "${remote_state_storage_container_name}", 7 | [switch]$auto_approve 8 | ) 9 | 10 | # Check and Set Subscription ID 11 | $wasSubscriptionIdSet = $false 12 | if($null -eq $env:ARM_SUBSCRIPTION_ID -or $env:ARM_SUBSCRIPTION_ID -eq "") { 13 | Write-Host "Setting environment variable ARM_SUBSCRIPTION_ID" 14 | $subscriptionId = $(az account show --query id -o tsv) 15 | if($null -eq $subscriptionId -or $subscriptionId -eq "") { 16 | Write-Error "Subscription ID not found. Please ensure you are logged in to Azure and have selected a subscription. Use 'az account show' to check." 17 | return 18 | } 19 | $env:ARM_SUBSCRIPTION_ID = $subscriptionId 20 | $wasSubscriptionIdSet = $true 21 | Write-Host "Environment variable ARM_SUBSCRIPTION_ID set to $subscriptionId" 22 | } 23 | 24 | # Initialize the Terraform configuration 25 | $use_remote_state = $false 26 | if($remote_state_resource_group_name -ne "" -and $remote_state_storage_account_name -ne "" -and $remote_state_storage_container_name -ne "") { 27 | $use_remote_state = $true 28 | } else { 29 | $use_remote_state = $false 30 | } 31 | 32 | $command = "terraform" 33 | $arguments = @() 34 | $arguments += "-chdir=$root_module_folder_relative_path" 35 | $arguments += "init" 36 | if($use_remote_state) { 37 | $arguments += "-migrate-state" 38 | $arguments += "-backend-config=resource_group_name=$remote_state_resource_group_name" 39 | $arguments += "-backend-config=storage_account_name=$remote_state_storage_account_name" 40 | $arguments += "-backend-config=container_name=$remote_state_storage_container_name" 41 | $arguments += "-backend-config=key=terraform.tfstate" 42 | $arguments += "-backend-config=use_azuread_auth=true" 43 | } 44 | Write-Host "Running: $command $arguments" 45 | & $command $arguments 46 | 47 | # Run the Terraform plan 48 | $command = "terraform" 49 | $arguments = @() 50 | $arguments += "-chdir=$root_module_folder_relative_path" 51 | $arguments += "plan" 52 | if($destroy) { 53 | $arguments += "-destroy" 54 | } 55 | $arguments += "-out=tfplan" 56 | Write-Host "Running: $command $arguments" 57 | & $command $arguments 58 | 59 | # Review the Terraform plan 60 | $command = "terraform" 61 | $arguments = @() 62 | $arguments += "-chdir=$root_module_folder_relative_path" 63 | $arguments += "show" 64 | $arguments += "tfplan" 65 | Write-Host "Running: $command $arguments" 66 | & $command $arguments 67 | 68 | $runType = $destroy ? "DESTROY" : "CREATE OR UPDATE" 69 | if($auto_approve) { 70 | Write-Host "Auto-approving the run to $runType the resources." 71 | } else { 72 | Write-Host "" 73 | $deployApproved = Read-Host -Prompt "Type 'yes' and hit Enter to $runType the resources." 74 | Write-Host "" 75 | 76 | if($deployApproved -ne "yes") { 77 | Write-Error "Deployment was not approved. Exiting..." 78 | exit 1 79 | } 80 | } 81 | 82 | # Apply the Terraform plan 83 | $command = "terraform" 84 | $arguments = @() 85 | $arguments += "-chdir=$root_module_folder_relative_path" 86 | $arguments += "apply" 87 | $arguments += "tfplan" 88 | Write-Host "Running: $command $arguments" 89 | & $command $arguments 90 | 91 | # Check and Unset Subscription ID 92 | if($wasSubscriptionIdSet) { 93 | Write-Host "Unsetting environment variable ARM_SUBSCRIPTION_ID" 94 | $env:ARM_SUBSCRIPTION_ID = $null 95 | Write-Host "Environment variable ARM_SUBSCRIPTION_ID unset" 96 | } 97 | -------------------------------------------------------------------------------- /alz/local/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 4.20" 7 | } 8 | azapi = { 9 | source = "azure/azapi" 10 | version = "~> 2.2" 11 | } 12 | local = { 13 | source = "hashicorp/local" 14 | version = "~> 2.4" 15 | } 16 | random = { 17 | source = "hashicorp/random" 18 | version = "~> 3.5" 19 | } 20 | http = { 21 | source = "hashicorp/http" 22 | version = "~> 3.4" 23 | } 24 | } 25 | } 26 | 27 | provider "azurerm" { 28 | subscription_id = var.bootstrap_subscription_id == "" ? null : var.bootstrap_subscription_id 29 | features { 30 | resource_group { 31 | prevent_deletion_if_contains_resources = false 32 | } 33 | storage { 34 | data_plane_available = false 35 | } 36 | } 37 | storage_use_azuread = true 38 | } 39 | -------------------------------------------------------------------------------- /modules/azure/container_instances.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_container_group" "alz" { 2 | for_each = var.use_self_hosted_agents ? var.agent_container_instances : {} 3 | name = each.value.container_instance_name 4 | location = var.azure_location 5 | resource_group_name = azurerm_resource_group.agents[0].name 6 | ip_address_type = var.use_private_networking ? "Private" : "None" 7 | os_type = "Linux" 8 | subnet_ids = var.use_private_networking ? [azurerm_subnet.container_instances[0].id] : [] 9 | zones = length(local.bootstrap_location_zones) == 0 ? null : each.value.zones 10 | 11 | identity { 12 | type = "UserAssigned" 13 | identity_ids = [azurerm_user_assigned_identity.container_instances[0].id] 14 | } 15 | 16 | image_registry_credential { 17 | server = azurerm_container_registry.alz[0].login_server 18 | user_assigned_identity_id = azurerm_user_assigned_identity.container_instances[0].id 19 | } 20 | 21 | container { 22 | name = each.value.container_instance_name 23 | image = "${azurerm_container_registry.alz[0].login_server}/${var.container_registry_image_name}:${var.container_registry_image_tag}" 24 | 25 | 26 | cpu = each.value.cpu 27 | memory = each.value.memory 28 | cpu_limit = each.value.cpu_max 29 | memory_limit = each.value.memory_max 30 | 31 | ports { 32 | port = 80 33 | protocol = "TCP" 34 | } 35 | 36 | environment_variables = merge({ 37 | (var.agent_organization_environment_variable) = var.agent_organization_url 38 | (var.agent_name_environment_variable) = each.value.agent_name 39 | }, var.use_agent_pool_environment_variable ? { 40 | (var.agent_pool_environment_variable) = var.agent_pool_name 41 | } : {}) 42 | 43 | secure_environment_variables = { 44 | (var.agent_token_environment_variable) = var.agent_token 45 | } 46 | } 47 | 48 | depends_on = [azurerm_container_registry_task_schedule_run_now.alz] 49 | } 50 | 51 | resource "azurerm_user_assigned_identity" "container_instances" { 52 | count = var.use_self_hosted_agents ? 1 : 0 53 | location = var.azure_location 54 | name = var.agent_container_instance_managed_identity_name 55 | resource_group_name = azurerm_resource_group.agents[0].name 56 | } 57 | -------------------------------------------------------------------------------- /modules/azure/container_registry.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_container_registry" "alz" { 2 | count = var.use_self_hosted_agents ? 1 : 0 3 | name = var.container_registry_name 4 | resource_group_name = azurerm_resource_group.agents[0].name 5 | location = var.azure_location 6 | sku = var.use_private_networking ? "Premium" : "Basic" 7 | public_network_access_enabled = !var.use_private_networking 8 | zone_redundancy_enabled = var.use_private_networking 9 | network_rule_bypass_option = var.use_private_networking ? "AzureServices" : "None" 10 | } 11 | 12 | resource "azurerm_container_registry_task" "alz" { 13 | count = var.use_self_hosted_agents ? 1 : 0 14 | name = "image-build-task" 15 | container_registry_id = azurerm_container_registry.alz[0].id 16 | platform { 17 | os = "Linux" 18 | } 19 | docker_step { 20 | dockerfile_path = var.container_registry_dockerfile_name 21 | context_path = var.container_registry_dockerfile_repository_folder_url 22 | context_access_token = "a" # This is a dummy value becuase the context_access_token should not be required in the provider 23 | image_names = ["${var.container_registry_image_name}:${var.container_registry_image_tag}"] 24 | } 25 | identity { 26 | type = "SystemAssigned" # Note this has to be a System Assigned Identity to work with private networking and `network_rule_bypass_option` set to `AzureServices` 27 | } 28 | registry_credential { 29 | custom { 30 | login_server = azurerm_container_registry.alz[0].login_server 31 | identity = "[system]" 32 | } 33 | } 34 | } 35 | 36 | resource "azurerm_container_registry_task_schedule_run_now" "alz" { 37 | count = var.use_self_hosted_agents ? 1 : 0 38 | container_registry_task_id = azurerm_container_registry_task.alz[0].id 39 | lifecycle { 40 | replace_triggered_by = [azurerm_container_registry_task.alz] 41 | } 42 | depends_on = [azurerm_role_assignment.container_registry_push_for_task] 43 | } 44 | 45 | resource "azurerm_role_assignment" "container_registry_pull_for_container_instance" { 46 | count = var.use_self_hosted_agents ? 1 : 0 47 | scope = azurerm_container_registry.alz[0].id 48 | role_definition_name = "AcrPull" 49 | principal_id = azurerm_user_assigned_identity.container_instances[0].principal_id 50 | } 51 | 52 | resource "azurerm_role_assignment" "container_registry_push_for_task" { 53 | count = var.use_self_hosted_agents ? 1 : 0 54 | scope = azurerm_container_registry.alz[0].id 55 | role_definition_name = "AcrPush" 56 | principal_id = azurerm_container_registry_task.alz[0].identity[0].principal_id 57 | } 58 | -------------------------------------------------------------------------------- /modules/azure/data.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_subscription" "alz" { 2 | for_each = local.subscription_ids 3 | subscription_id = each.key 4 | } 5 | 6 | data "azurerm_management_group" "alz" { 7 | name = var.root_parent_management_group_id 8 | } 9 | 10 | data "http" "ip" { 11 | count = var.use_private_networking && var.use_self_hosted_agents && var.allow_storage_access_from_my_ip ? 1 : 0 12 | url = "https://api.ipify.org/" 13 | retry { 14 | attempts = 5 15 | max_delay_ms = 1000 16 | min_delay_ms = 500 17 | } 18 | } 19 | 20 | module "regions" { 21 | source = "Azure/avm-utl-regions/azurerm" 22 | version = "0.5.2" 23 | use_cached_data = false 24 | availability_zones_filter = false 25 | recommended_filter = false 26 | } 27 | 28 | locals { 29 | regions = { for region in module.regions.regions_by_name : region.name => { 30 | display_name = region.display_name 31 | zones = region.zones == null ? [] : region.zones 32 | } 33 | } 34 | bootstrap_location_zones = local.regions[var.azure_location].zones 35 | } 36 | -------------------------------------------------------------------------------- /modules/azure/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | audience = "api://AzureADTokenExchange" 3 | } 4 | 5 | locals { 6 | subscription_ids = { for subscription_id in distinct(var.target_subscriptions) : subscription_id => subscription_id } 7 | } 8 | -------------------------------------------------------------------------------- /modules/azure/managed_identity.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_user_assigned_identity" "alz" { 2 | for_each = var.user_assigned_managed_identities 3 | location = var.azure_location 4 | name = each.value 5 | resource_group_name = azurerm_resource_group.identity.name 6 | } 7 | 8 | resource "azurerm_federated_identity_credential" "alz" { 9 | for_each = var.federated_credentials 10 | name = each.value.federated_credential_name 11 | resource_group_name = azurerm_resource_group.identity.name 12 | audience = [local.audience] 13 | issuer = each.value.federated_credential_issuer 14 | parent_id = azurerm_user_assigned_identity.alz[each.value.user_assigned_managed_identity_key].id 15 | subject = each.value.federated_credential_subject 16 | } 17 | -------------------------------------------------------------------------------- /modules/azure/networking.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_virtual_network" "alz" { 2 | count = var.use_private_networking && var.use_self_hosted_agents ? 1 : 0 3 | name = var.virtual_network_name 4 | location = var.azure_location 5 | resource_group_name = azurerm_resource_group.network[0].name 6 | address_space = [var.virtual_network_address_space] 7 | } 8 | 9 | resource "azurerm_public_ip" "alz" { 10 | count = var.use_private_networking && var.use_self_hosted_agents ? 1 : 0 11 | name = var.public_ip_name 12 | location = var.azure_location 13 | resource_group_name = azurerm_resource_group.network[0].name 14 | allocation_method = "Static" 15 | sku = "Standard" 16 | } 17 | 18 | resource "azurerm_nat_gateway" "alz" { 19 | count = var.use_private_networking && var.use_self_hosted_agents ? 1 : 0 20 | name = var.nat_gateway_name 21 | location = var.azure_location 22 | resource_group_name = azurerm_resource_group.network[0].name 23 | sku_name = "Standard" 24 | } 25 | 26 | resource "azurerm_nat_gateway_public_ip_association" "alz" { 27 | count = var.use_private_networking && var.use_self_hosted_agents ? 1 : 0 28 | nat_gateway_id = azurerm_nat_gateway.alz[0].id 29 | public_ip_address_id = azurerm_public_ip.alz[0].id 30 | } 31 | 32 | resource "azurerm_subnet" "container_instances" { 33 | count = var.use_private_networking && var.use_self_hosted_agents ? 1 : 0 34 | name = var.virtual_network_subnet_name_container_instances 35 | resource_group_name = azurerm_resource_group.network[0].name 36 | virtual_network_name = azurerm_virtual_network.alz[0].name 37 | address_prefixes = [var.virtual_network_subnet_address_prefix_container_instances] 38 | private_endpoint_network_policies = "Enabled" 39 | delegation { 40 | name = "aci-delegation" 41 | service_delegation { 42 | name = "Microsoft.ContainerInstance/containerGroups" 43 | actions = ["Microsoft.Network/virtualNetworks/subnets/action"] 44 | } 45 | } 46 | } 47 | 48 | resource "azurerm_subnet_nat_gateway_association" "container_instances" { 49 | count = var.use_private_networking && var.use_self_hosted_agents ? 1 : 0 50 | subnet_id = azurerm_subnet.container_instances[0].id 51 | nat_gateway_id = azurerm_nat_gateway.alz[0].id 52 | } 53 | 54 | resource "azurerm_subnet" "private_endpoints" { 55 | count = var.use_private_networking && var.use_self_hosted_agents ? 1 : 0 56 | name = var.virtual_network_subnet_name_private_endpoints 57 | resource_group_name = azurerm_resource_group.network[0].name 58 | virtual_network_name = azurerm_virtual_network.alz[0].name 59 | address_prefixes = [var.virtual_network_subnet_address_prefix_private_endpoints] 60 | private_endpoint_network_policies = "Enabled" 61 | } 62 | -------------------------------------------------------------------------------- /modules/azure/outputs.tf: -------------------------------------------------------------------------------- 1 | output "user_assigned_managed_identity_client_ids" { 2 | value = { for key, value in var.user_assigned_managed_identities : key => azurerm_user_assigned_identity.alz[key].client_id } 3 | } 4 | 5 | output "role_assignments" { 6 | value = local.role_assignments 7 | } 8 | -------------------------------------------------------------------------------- /modules/azure/private_endpoints.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | private_endpoints = var.use_private_networking && var.use_self_hosted_agents ? merge(var.create_storage_account ? { 3 | storage_account = { 4 | name = var.storage_account_private_endpoint_name 5 | resource_id = azurerm_storage_account.alz[0].id 6 | dns_record = "privatelink.blob.core.windows.net" 7 | sub_resource = "blob" 8 | } 9 | } : {}, 10 | { 11 | container_registry = { 12 | name = var.container_registry_private_endpoint_name 13 | resource_id = azurerm_container_registry.alz[0].id 14 | dns_record = "privatelink.azurecr.io" 15 | sub_resource = "registry" 16 | } 17 | }) : {} 18 | } 19 | 20 | resource "azurerm_private_dns_zone" "alz" { 21 | for_each = local.private_endpoints 22 | name = each.value.dns_record 23 | resource_group_name = azurerm_resource_group.network[0].name 24 | } 25 | 26 | resource "azurerm_private_dns_zone_virtual_network_link" "alz" { 27 | for_each = local.private_endpoints 28 | name = each.value.name 29 | resource_group_name = azurerm_resource_group.network[0].name 30 | private_dns_zone_name = azurerm_private_dns_zone.alz[each.key].name 31 | virtual_network_id = azurerm_virtual_network.alz[0].id 32 | } 33 | 34 | resource "azurerm_private_endpoint" "alz" { 35 | for_each = local.private_endpoints 36 | name = each.value.name 37 | location = var.azure_location 38 | resource_group_name = azurerm_resource_group.network[0].name 39 | subnet_id = azurerm_subnet.private_endpoints[0].id 40 | 41 | private_service_connection { 42 | name = each.value.name 43 | private_connection_resource_id = each.value.resource_id 44 | subresource_names = [each.value.sub_resource] 45 | is_manual_connection = false 46 | } 47 | 48 | private_dns_zone_group { 49 | name = each.value.name 50 | private_dns_zone_ids = [azurerm_private_dns_zone.alz[each.key].id] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /modules/azure/resource_groups.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_resource_group" "state" { 2 | count = var.create_storage_account ? 1 : 0 3 | name = var.resource_group_state_name 4 | location = var.azure_location 5 | } 6 | 7 | resource "azurerm_resource_group" "identity" { 8 | name = var.resource_group_identity_name 9 | location = var.azure_location 10 | } 11 | 12 | resource "azurerm_resource_group" "agents" { 13 | count = var.use_self_hosted_agents ? 1 : 0 14 | name = var.resource_group_agents_name 15 | location = var.azure_location 16 | } 17 | 18 | resource "azurerm_resource_group" "network" { 19 | count = var.use_private_networking ? 1 : 0 20 | name = var.resource_group_network_name 21 | location = var.azure_location 22 | } 23 | -------------------------------------------------------------------------------- /modules/azure/resource_providers.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | resource_providers_by_subscriptions = flatten([ 3 | for key, value in data.azurerm_subscription.alz : [ 4 | for resource_provider in var.resource_providers : 5 | { 6 | subscription_id = value.subscription_id 7 | resource_provider = resource_provider 8 | } 9 | ] 10 | ]) 11 | 12 | resource_providers_to_register = { 13 | for resource_provider in local.resource_providers_by_subscriptions : "${resource_provider.subscription_id}_${resource_provider.resource_provider}" => resource_provider 14 | } 15 | } 16 | 17 | resource "azapi_resource_action" "resource_provider_registration" { 18 | for_each = local.resource_providers_to_register 19 | type = "Microsoft.Resources/subscriptions@2021-04-01" 20 | resource_id = "/subscriptions/${each.value.subscription_id}" 21 | action = "providers/${each.value.resource_provider}/register" 22 | method = "POST" 23 | } 24 | 25 | -------------------------------------------------------------------------------- /modules/azure/role_assignments.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | role_assignments = { for key, value in var.role_assignments : key => { 3 | user_assigned_managed_identity_key = value.user_assigned_managed_identity_key 4 | custom_role_definition_key = value.custom_role_definition_key 5 | scope = value.scope 6 | principal_id = azurerm_user_assigned_identity.alz[value.user_assigned_managed_identity_key].principal_id 7 | } } 8 | 9 | additional_role_assignments = { for assignment in flatten([ 10 | for key, value in var.role_assignments : [ 11 | for princial_key, principal_value in var.additional_role_assignment_principal_ids : { 12 | composite_key = "${value.scope}-${value.custom_role_definition_key}-${princial_key}" 13 | user_assigned_managed_identity_key = "${value.scope}-${value.custom_role_definition_key}-${princial_key}" 14 | custom_role_definition_key = value.custom_role_definition_key 15 | scope = value.scope 16 | principal_id = principal_value 17 | } 18 | ]]) : assignment.composite_key => { 19 | user_assigned_managed_identity_key = assignment.user_assigned_managed_identity_key 20 | custom_role_definition_key = assignment.custom_role_definition_key 21 | scope = assignment.scope 22 | principal_id = assignment.principal_id 23 | } } 24 | 25 | combined_role_assignments = merge(local.role_assignments, local.additional_role_assignments) 26 | 27 | subscription_role_assignments = { for assignment in flatten([ 28 | for key, value in local.combined_role_assignments : [ 29 | for subscription_id, subscription in data.azurerm_subscription.alz : { 30 | key = "${value.user_assigned_managed_identity_key}-${value.custom_role_definition_key}-${subscription_id}" 31 | scope = subscription.id 32 | role_definition_id = "${subscription.id}${azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id}" 33 | principal_id = value.principal_id 34 | } 35 | ] if value.scope == "subscription" 36 | ]) : assignment.key => { 37 | scope = assignment.scope 38 | role_definition_id = assignment.role_definition_id 39 | principal_id = assignment.principal_id 40 | } } 41 | 42 | management_group_role_assignments = { 43 | for key, value in local.combined_role_assignments : key => { 44 | scope = data.azurerm_management_group.alz.id 45 | role_definition_id = azurerm_role_definition.alz[value.custom_role_definition_key].role_definition_resource_id 46 | principal_id = value.principal_id 47 | } if value.scope == "management_group" 48 | } 49 | final_role_assignments = merge(local.subscription_role_assignments, local.management_group_role_assignments) 50 | } 51 | 52 | resource "azurerm_role_assignment" "alz" { 53 | for_each = local.final_role_assignments 54 | scope = each.value.scope 55 | role_definition_id = each.value.role_definition_id 56 | principal_id = each.value.principal_id 57 | } 58 | -------------------------------------------------------------------------------- /modules/azure/role_definitions.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_role_definition" "alz" { 2 | for_each = var.custom_role_definitions 3 | name = each.value.name 4 | scope = data.azurerm_management_group.alz.id 5 | description = each.value.description 6 | 7 | permissions { 8 | actions = each.value.permissions.actions 9 | not_actions = each.value.permissions.not_actions 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/azure/storage.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_storage_account" "alz" { 2 | count = var.create_storage_account ? 1 : 0 3 | name = var.storage_account_name 4 | resource_group_name = azurerm_resource_group.state[0].name 5 | location = var.azure_location 6 | account_tier = "Standard" 7 | account_replication_type = var.storage_account_replication_type 8 | allow_nested_items_to_be_public = false 9 | shared_access_key_enabled = false 10 | public_network_access_enabled = var.use_private_networking && var.use_self_hosted_agents && !var.allow_storage_access_from_my_ip ? false : true 11 | lifecycle { 12 | ignore_changes = [queue_properties, static_website] 13 | } 14 | } 15 | 16 | resource "azurerm_storage_account_network_rules" "alz" { 17 | count = var.create_storage_account && var.use_private_networking ? 1 : 0 18 | storage_account_id = azurerm_storage_account.alz[0].id 19 | default_action = "Deny" 20 | ip_rules = var.allow_storage_access_from_my_ip ? [data.http.ip[0].response_body] : [] 21 | bypass = ["None"] 22 | } 23 | 24 | data "azapi_resource_id" "storage_account_blob_service" { 25 | count = var.create_storage_account ? 1 : 0 26 | type = "Microsoft.Storage/storageAccounts/blobServices@2022-09-01" 27 | parent_id = azurerm_storage_account.alz[0].id 28 | name = "default" 29 | } 30 | 31 | resource "azapi_resource" "storage_account_container" { 32 | count = var.create_storage_account ? 1 : 0 33 | type = "Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01" 34 | parent_id = data.azapi_resource_id.storage_account_blob_service[0].id 35 | name = var.storage_container_name 36 | body = { 37 | properties = { 38 | publicAccess = "None" 39 | } 40 | } 41 | schema_validation_enabled = false 42 | depends_on = [azurerm_storage_account_network_rules.alz] 43 | } 44 | 45 | resource "azurerm_role_assignment" "alz_storage_container" { 46 | for_each = var.create_storage_account ? var.user_assigned_managed_identities : {} 47 | scope = azapi_resource.storage_account_container[0].id 48 | role_definition_name = "Storage Blob Data Owner" 49 | principal_id = azurerm_user_assigned_identity.alz[each.key].principal_id 50 | } 51 | 52 | resource "azurerm_role_assignment" "alz_storage_container_additional" { 53 | for_each = var.create_storage_account ? var.additional_role_assignment_principal_ids : {} 54 | scope = azapi_resource.storage_account_container[0].id 55 | role_definition_name = "Storage Blob Data Owner" 56 | principal_id = each.value 57 | } 58 | 59 | # These role assignments are a temporary addition to handle this issue in the Terraform CLI: https://github.com/hashicorp/terraform/issues/36595 60 | # They will be removed once the issue has been resolved 61 | resource "azurerm_role_assignment" "alz_storage_reader" { 62 | for_each = var.create_storage_account ? var.user_assigned_managed_identities : {} 63 | scope = azurerm_storage_account.alz[0].id 64 | role_definition_name = "Reader" 65 | principal_id = azurerm_user_assigned_identity.alz[each.key].principal_id 66 | } 67 | 68 | resource "azurerm_role_assignment" "alz_storage_reader_additional" { 69 | for_each = var.create_storage_account ? var.additional_role_assignment_principal_ids : {} 70 | scope = azurerm_storage_account.alz[0].id 71 | role_definition_name = "Reader" 72 | principal_id = each.value 73 | } 74 | -------------------------------------------------------------------------------- /modules/azure/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | azurerm = { 4 | source = "hashicorp/azurerm" 5 | version = "~> 4.20" 6 | } 7 | azapi = { 8 | source = "azure/azapi" 9 | version = "~> 2.2" 10 | } 11 | http = { 12 | source = "hashicorp/http" 13 | version = "~> 3.4" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/azure_devops/agent_pool.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_agent_pool" "alz" { 2 | count = var.use_self_hosted_agents ? 1 : 0 3 | name = var.agent_pool_name 4 | auto_provision = false 5 | auto_update = true 6 | } 7 | 8 | resource "azuredevops_agent_queue" "alz" { 9 | count = var.use_self_hosted_agents ? 1 : 0 10 | project_id = local.project_id 11 | agent_pool_id = azuredevops_agent_pool.alz[0].id 12 | } 13 | -------------------------------------------------------------------------------- /modules/azure_devops/environment.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_environment" "alz" { 2 | for_each = var.environments 3 | name = each.value.environment_name 4 | project_id = local.project_id 5 | } 6 | -------------------------------------------------------------------------------- /modules/azure_devops/groups.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_group" "alz_approvers" { 2 | scope = local.project_id 3 | display_name = var.group_name 4 | description = "Approvers for the Landing Zone Terraform Apply" 5 | } 6 | 7 | data "azuredevops_users" "alz" { 8 | for_each = { for approver in var.approvers : approver => approver } 9 | principal_name = each.key 10 | lifecycle { 11 | postcondition { 12 | condition = length(self.users) > 0 13 | error_message = "No user account found for ${each.value}, check you have entered a valid user principal name..." 14 | } 15 | } 16 | } 17 | 18 | locals { 19 | approvers = toset(flatten([for approver in data.azuredevops_users.alz : 20 | [for user in approver.users : user.descriptor] 21 | ])) 22 | } 23 | 24 | resource "azuredevops_group_membership" "alz_approvers" { 25 | group = azuredevops_group.alz_approvers.descriptor 26 | members = local.approvers 27 | } 28 | -------------------------------------------------------------------------------- /modules/azure_devops/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | organization_url = startswith(lower(var.organization_name), "https://") || startswith(lower(var.organization_name), "http://") ? var.organization_name : (var.use_legacy_organization_url ? "https://${var.organization_name}.visualstudio.com" : "https://dev.azure.com/${var.organization_name}") 3 | } 4 | 5 | locals { 6 | apply_key = "apply" 7 | } 8 | 9 | locals { 10 | authentication_scheme_workload_identity_federation = "WorkloadIdentityFederation" 11 | } 12 | 13 | locals { 14 | default_branch = "refs/heads/main" 15 | } 16 | 17 | locals { 18 | repository_name_templates = var.use_template_repository ? var.repository_name_templates : var.repository_name 19 | } 20 | -------------------------------------------------------------------------------- /modules/azure_devops/locals_pipelines.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | pipelines = { for key, value in var.pipelines : key => { 3 | pipeline_name = value.pipeline_name 4 | file = azuredevops_git_repository_file.alz[value.pipeline_file_name].file 5 | environments = [for environment_key in value.environment_keys : 6 | { 7 | environment_key = environment_key 8 | environment_id = azuredevops_environment.alz[environment_key].id 9 | } 10 | ] 11 | service_connections = [for service_connection_key in value.service_connection_keys : 12 | { 13 | service_connection_key = service_connection_key 14 | service_connection_id = azuredevops_serviceendpoint_azurerm.alz[service_connection_key].id 15 | } 16 | ] 17 | } 18 | } 19 | 20 | pipeline_environments = flatten([for pipeline_key, pipeline in local.pipelines : 21 | [for environment in pipeline.environments : { 22 | pipeline_key = pipeline_key 23 | environment_key = environment.environment_key 24 | pipeline_id = azuredevops_build_definition.alz[pipeline_key].id 25 | environment_id = environment.environment_id 26 | } 27 | ] 28 | ]) 29 | 30 | pipeline_service_connections = flatten([for pipeline_key, pipeline in local.pipelines : 31 | [for service_connection in pipeline.service_connections : { 32 | pipeline_key = pipeline_key 33 | service_connection_key = service_connection.service_connection_key 34 | pipeline_id = azuredevops_build_definition.alz[pipeline_key].id 35 | service_connection_id = service_connection.service_connection_id 36 | } 37 | ] 38 | ]) 39 | 40 | pipeline_environments_map = { for pipeline_environment in local.pipeline_environments : "${pipeline_environment.pipeline_key}-${pipeline_environment.environment_key}" => { 41 | pipeline_id = pipeline_environment.pipeline_id 42 | environment_id = pipeline_environment.environment_id 43 | } 44 | } 45 | 46 | pipeline_service_connections_map = { for pipeline_service_connection in local.pipeline_service_connections : "${pipeline_service_connection.pipeline_key}-${pipeline_service_connection.service_connection_key}" => { 47 | pipeline_id = pipeline_service_connection.pipeline_id 48 | service_connection_id = pipeline_service_connection.service_connection_id 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /modules/azure_devops/outputs.tf: -------------------------------------------------------------------------------- 1 | output "organization_url" { 2 | value = local.organization_url 3 | } 4 | 5 | output "subjects" { 6 | value = { for key, value in var.environments : key => azuredevops_serviceendpoint_azurerm.alz[key].workload_identity_federation_subject } 7 | } 8 | 9 | output "issuers" { 10 | value = { for key, value in var.environments : key => azuredevops_serviceendpoint_azurerm.alz[key].workload_identity_federation_issuer } 11 | } 12 | 13 | output "agent_pool_name" { 14 | value = var.use_self_hosted_agents ? azuredevops_agent_pool.alz[0].name : null 15 | } 16 | -------------------------------------------------------------------------------- /modules/azure_devops/pipeline.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_build_definition" "alz" { 2 | for_each = local.pipelines 3 | project_id = local.project_id 4 | name = each.value.pipeline_name 5 | 6 | ci_trigger { 7 | use_yaml = true 8 | } 9 | 10 | repository { 11 | repo_type = "TfsGit" 12 | repo_id = azuredevops_git_repository.alz.id 13 | branch_name = azuredevops_git_repository.alz.default_branch 14 | yml_path = each.value.file 15 | } 16 | } 17 | 18 | resource "azuredevops_pipeline_authorization" "alz_environment" { 19 | for_each = local.pipeline_environments_map 20 | project_id = local.project_id 21 | resource_id = each.value.environment_id 22 | type = "environment" 23 | pipeline_id = each.value.pipeline_id 24 | } 25 | 26 | resource "azuredevops_pipeline_authorization" "alz_service_connection" { 27 | for_each = local.pipeline_service_connections_map 28 | project_id = local.project_id 29 | resource_id = each.value.service_connection_id 30 | type = "endpoint" 31 | pipeline_id = each.value.pipeline_id 32 | } 33 | 34 | resource "azuredevops_pipeline_authorization" "alz_agent_pool" { 35 | for_each = var.use_self_hosted_agents ? local.pipelines : {} 36 | project_id = local.project_id 37 | resource_id = azuredevops_agent_queue.alz[0].id 38 | type = "queue" 39 | pipeline_id = azuredevops_build_definition.alz[each.key].id 40 | } 41 | -------------------------------------------------------------------------------- /modules/azure_devops/project.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_project" "alz" { 2 | count = var.create_project ? 1 : 0 3 | name = var.project_name 4 | } 5 | 6 | data "azuredevops_project" "alz" { 7 | count = var.create_project ? 0 : 1 8 | name = var.project_name 9 | } 10 | 11 | locals { 12 | project_id = var.create_project ? azuredevops_project.alz[0].id : data.azuredevops_project.alz[0].id 13 | } 14 | -------------------------------------------------------------------------------- /modules/azure_devops/repository_module.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_git_repository" "alz" { 2 | depends_on = [azuredevops_environment.alz] 3 | project_id = local.project_id 4 | name = var.repository_name 5 | default_branch = local.default_branch 6 | initialization { 7 | init_type = "Clean" 8 | } 9 | } 10 | 11 | resource "azuredevops_git_repository_file" "alz" { 12 | for_each = var.repository_files 13 | repository_id = azuredevops_git_repository.alz.id 14 | file = each.key 15 | content = each.value.content 16 | branch = local.default_branch 17 | commit_message = "[skip ci]" 18 | overwrite_on_create = true 19 | } 20 | 21 | resource "azuredevops_branch_policy_min_reviewers" "alz" { 22 | depends_on = [azuredevops_git_repository_file.alz] 23 | project_id = local.project_id 24 | 25 | enabled = length(var.approvers) > 1 && var.create_branch_policies 26 | blocking = true 27 | 28 | settings { 29 | reviewer_count = 1 30 | submitter_can_vote = false 31 | last_pusher_cannot_approve = true 32 | allow_completion_with_rejects_or_waits = false 33 | on_push_reset_approved_votes = true 34 | 35 | scope { 36 | repository_id = azuredevops_git_repository.alz.id 37 | repository_ref = azuredevops_git_repository.alz.default_branch 38 | match_type = "Exact" 39 | } 40 | } 41 | } 42 | 43 | resource "azuredevops_branch_policy_merge_types" "alz" { 44 | depends_on = [azuredevops_git_repository_file.alz] 45 | project_id = local.project_id 46 | 47 | enabled = var.create_branch_policies 48 | blocking = true 49 | 50 | settings { 51 | allow_squash = true 52 | allow_rebase_and_fast_forward = false 53 | allow_basic_no_fast_forward = false 54 | allow_rebase_with_merge = false 55 | 56 | scope { 57 | repository_id = azuredevops_git_repository.alz.id 58 | repository_ref = azuredevops_git_repository.alz.default_branch 59 | match_type = "Exact" 60 | } 61 | } 62 | } 63 | 64 | resource "azuredevops_branch_policy_build_validation" "alz" { 65 | depends_on = [azuredevops_git_repository_file.alz] 66 | project_id = local.project_id 67 | 68 | enabled = var.create_branch_policies 69 | blocking = true 70 | 71 | settings { 72 | display_name = "Terraform Validation" 73 | build_definition_id = azuredevops_build_definition.alz["ci"].id 74 | valid_duration = 720 75 | 76 | scope { 77 | repository_id = azuredevops_git_repository.alz.id 78 | repository_ref = azuredevops_git_repository.alz.default_branch 79 | match_type = "Exact" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /modules/azure_devops/repository_templates.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_git_repository" "alz_templates" { 2 | count = var.use_template_repository ? 1 : 0 3 | project_id = local.project_id 4 | name = var.repository_name_templates 5 | default_branch = local.default_branch 6 | initialization { 7 | init_type = "Clean" 8 | } 9 | } 10 | 11 | resource "azuredevops_git_repository_file" "alz_templates" { 12 | for_each = var.use_template_repository ? var.template_repository_files : {} 13 | repository_id = azuredevops_git_repository.alz_templates[0].id 14 | file = each.key 15 | content = each.value.content 16 | branch = local.default_branch 17 | commit_message = "[skip ci]" 18 | overwrite_on_create = true 19 | } 20 | 21 | resource "azuredevops_branch_policy_min_reviewers" "alz_templates" { 22 | count = var.use_template_repository ? 1 : 0 23 | depends_on = [azuredevops_git_repository_file.alz_templates] 24 | project_id = local.project_id 25 | 26 | enabled = length(var.approvers) > 1 && var.create_branch_policies 27 | blocking = true 28 | 29 | settings { 30 | reviewer_count = 1 31 | submitter_can_vote = false 32 | last_pusher_cannot_approve = true 33 | allow_completion_with_rejects_or_waits = false 34 | on_push_reset_approved_votes = true 35 | 36 | scope { 37 | repository_id = azuredevops_git_repository.alz_templates[0].id 38 | repository_ref = azuredevops_git_repository.alz_templates[0].default_branch 39 | match_type = "Exact" 40 | } 41 | } 42 | } 43 | 44 | resource "azuredevops_branch_policy_merge_types" "alz_templates" { 45 | count = var.use_template_repository ? 1 : 0 46 | depends_on = [azuredevops_git_repository_file.alz_templates] 47 | project_id = local.project_id 48 | 49 | enabled = var.create_branch_policies 50 | blocking = true 51 | 52 | settings { 53 | allow_squash = true 54 | allow_rebase_and_fast_forward = false 55 | allow_basic_no_fast_forward = false 56 | allow_rebase_with_merge = false 57 | 58 | scope { 59 | repository_id = azuredevops_git_repository.alz_templates[0].id 60 | repository_ref = azuredevops_git_repository.alz_templates[0].default_branch 61 | match_type = "Exact" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /modules/azure_devops/service_connections.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_serviceendpoint_azurerm" "alz" { 2 | for_each = var.environments 3 | project_id = local.project_id 4 | service_endpoint_name = each.value.service_connection_name 5 | description = "Managed by Terraform" 6 | service_endpoint_authentication_scheme = local.authentication_scheme_workload_identity_federation 7 | 8 | credentials { 9 | serviceprincipalid = var.managed_identity_client_ids[each.key] 10 | } 11 | 12 | azurerm_spn_tenantid = var.azure_tenant_id 13 | azurerm_subscription_id = var.azure_subscription_id 14 | azurerm_subscription_name = var.azure_subscription_name 15 | } 16 | 17 | resource "azuredevops_check_approval" "alz" { 18 | count = length(var.approvers) == 0 ? 0 : 1 19 | project_id = local.project_id 20 | target_resource_id = azuredevops_serviceendpoint_azurerm.alz[local.apply_key].id 21 | target_resource_type = "endpoint" 22 | 23 | requester_can_approve = length(var.approvers) == 1 24 | approvers = [ 25 | azuredevops_group.alz_approvers.origin_id 26 | ] 27 | 28 | timeout = 43200 29 | } 30 | 31 | resource "azuredevops_check_exclusive_lock" "alz" { 32 | for_each = var.environments 33 | project_id = local.project_id 34 | target_resource_id = azuredevops_serviceendpoint_azurerm.alz[each.key].id 35 | target_resource_type = "endpoint" 36 | timeout = 43200 37 | } 38 | 39 | resource "azuredevops_check_required_template" "alz" { 40 | for_each = var.environments 41 | project_id = local.project_id 42 | target_resource_id = azuredevops_serviceendpoint_azurerm.alz[each.key].id 43 | target_resource_type = "endpoint" 44 | 45 | dynamic "required_template" { 46 | for_each = each.value.service_connection_required_templates 47 | content { 48 | repository_type = "azuregit" 49 | repository_name = "${var.project_name}/${local.repository_name_templates}" 50 | repository_ref = "refs/heads/main" 51 | template_path = required_template.value 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modules/azure_devops/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | azuredevops = { 4 | source = "microsoft/azuredevops" 5 | version = "~> 1.7" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/azure_devops/variable_group.tf: -------------------------------------------------------------------------------- 1 | resource "azuredevops_variable_group" "example" { 2 | project_id = local.project_id 3 | name = var.variable_group_name 4 | description = var.variable_group_name 5 | allow_access = true 6 | 7 | variable { 8 | name = "BACKEND_AZURE_RESOURCE_GROUP_NAME" 9 | value = var.backend_azure_resource_group_name 10 | } 11 | 12 | variable { 13 | name = "BACKEND_AZURE_STORAGE_ACCOUNT_NAME" 14 | value = var.backend_azure_storage_account_name 15 | } 16 | 17 | variable { 18 | name = "BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME" 19 | value = var.backend_azure_storage_account_container_name 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/azure_devops/variables.tf: -------------------------------------------------------------------------------- 1 | variable "use_legacy_organization_url" { 2 | type = bool 3 | } 4 | 5 | variable "organization_name" { 6 | type = string 7 | } 8 | 9 | variable "create_project" { 10 | type = bool 11 | } 12 | 13 | variable "project_name" { 14 | type = string 15 | } 16 | 17 | variable "environments" { 18 | type = map(object({ 19 | environment_name = string 20 | service_connection_name = string 21 | service_connection_required_templates = list(string) 22 | })) 23 | } 24 | 25 | variable "pipelines" { 26 | type = map(object({ 27 | pipeline_name = string 28 | pipeline_file_name = string 29 | environment_keys = list(string) 30 | service_connection_keys = list(string) 31 | })) 32 | } 33 | 34 | variable "managed_identity_client_ids" { 35 | type = map(string) 36 | } 37 | 38 | variable "repository_name" { 39 | type = string 40 | } 41 | 42 | variable "repository_files" { 43 | type = map(object({ 44 | content = string 45 | })) 46 | } 47 | 48 | variable "template_repository_files" { 49 | type = map(object({ 50 | content = string 51 | })) 52 | } 53 | 54 | variable "variable_group_name" { 55 | type = string 56 | } 57 | 58 | variable "azure_tenant_id" { 59 | type = string 60 | } 61 | 62 | variable "azure_subscription_id" { 63 | type = string 64 | } 65 | 66 | variable "azure_subscription_name" { 67 | type = string 68 | } 69 | 70 | variable "backend_azure_resource_group_name" { 71 | type = string 72 | } 73 | 74 | variable "backend_azure_storage_account_name" { 75 | type = string 76 | } 77 | 78 | variable "backend_azure_storage_account_container_name" { 79 | type = string 80 | } 81 | 82 | variable "approvers" { 83 | type = list(string) 84 | } 85 | 86 | variable "group_name" { 87 | type = string 88 | } 89 | 90 | variable "use_template_repository" { 91 | type = bool 92 | } 93 | 94 | variable "repository_name_templates" { 95 | type = string 96 | } 97 | 98 | variable "agent_pool_name" { 99 | type = string 100 | } 101 | 102 | variable "use_self_hosted_agents" { 103 | type = bool 104 | } 105 | 106 | variable "create_branch_policies" { 107 | type = bool 108 | } 109 | -------------------------------------------------------------------------------- /modules/files/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | has_configuration_file = var.configuration_file_path != "" 3 | } 4 | 5 | locals { 6 | starter_module_files = { for file in fileset(var.starter_module_folder_path, "**") : file => { 7 | path = "${var.starter_module_folder_path}/${file}" 8 | } if(!local.has_configuration_file || !contains(var.built_in_configuration_file_names, file)) && !strcontains(file, var.starter_module_folder_path_exclusion) 9 | } 10 | 11 | additional_folders_files = length(var.additional_folders_path) != 0 ? merge( 12 | [for folder_path in var.additional_folders_path : { for file in fileset(folder_path, "**") : "${basename(folder_path)}/${file}" => { 13 | path = "${folder_path}/${file}" 14 | } 15 | }]...) : {} 16 | 17 | final_additional_files = concat(var.additional_files, local.has_configuration_file ? [var.configuration_file_path] : []) 18 | additional_repo_files = { for file in local.final_additional_files : basename(file) => { 19 | path = file 20 | } 21 | } 22 | all_repo_files = merge(local.starter_module_files, local.additional_repo_files, local.additional_folders_files) 23 | } 24 | -------------------------------------------------------------------------------- /modules/files/outputs.tf: -------------------------------------------------------------------------------- 1 | output "files" { 2 | value = local.all_repo_files 3 | } 4 | -------------------------------------------------------------------------------- /modules/files/variables.tf: -------------------------------------------------------------------------------- 1 | variable "starter_module_folder_path" { 2 | description = "Starter module folder path" 3 | type = string 4 | } 5 | 6 | variable "starter_module_folder_path_exclusion" { 7 | description = "Starter module folder path exclusion" 8 | type = string 9 | default = ".examples" 10 | } 11 | 12 | variable "additional_files" { 13 | description = "Additional files" 14 | type = list(string) 15 | default = [] 16 | } 17 | 18 | variable "configuration_file_path" { 19 | description = "Configuration file path" 20 | type = string 21 | default = "" 22 | } 23 | 24 | variable "built_in_configuration_file_names" { 25 | description = "Built-in configuration file name" 26 | type = list(string) 27 | default = ["config.yaml", "config-hub-and-spoke-vnet.yaml", "config-virtual-wan.yaml"] 28 | } 29 | 30 | variable "additional_folders_path" { 31 | description = "Additional folders" 32 | type = list(string) 33 | default = [] 34 | } 35 | -------------------------------------------------------------------------------- /modules/github/action_variables.tf: -------------------------------------------------------------------------------- 1 | resource "github_actions_environment_variable" "azure_plan_client_id" { 2 | for_each = var.environments 3 | repository = github_repository.alz.name 4 | environment = github_repository_environment.alz[each.key].environment 5 | variable_name = "AZURE_CLIENT_ID" 6 | value = var.managed_identity_client_ids[each.key] 7 | } 8 | 9 | resource "github_actions_variable" "azure_subscription_id" { 10 | repository = github_repository.alz.name 11 | variable_name = "AZURE_SUBSCRIPTION_ID" 12 | value = var.azure_subscription_id 13 | } 14 | 15 | resource "github_actions_variable" "azure_tenant_id" { 16 | repository = github_repository.alz.name 17 | variable_name = "AZURE_TENANT_ID" 18 | value = var.azure_tenant_id 19 | } 20 | 21 | resource "github_actions_variable" "backend_azure_resource_group_name" { 22 | repository = github_repository.alz.name 23 | variable_name = "BACKEND_AZURE_RESOURCE_GROUP_NAME" 24 | value = var.backend_azure_resource_group_name 25 | } 26 | 27 | resource "github_actions_variable" "backend_azure_storage_account_name" { 28 | repository = github_repository.alz.name 29 | variable_name = "BACKEND_AZURE_STORAGE_ACCOUNT_NAME" 30 | value = var.backend_azure_storage_account_name 31 | } 32 | 33 | resource "github_actions_variable" "backend_azure_storage_account_container_name" { 34 | repository = github_repository.alz.name 35 | variable_name = "BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME" 36 | value = var.backend_azure_storage_account_container_name 37 | } 38 | -------------------------------------------------------------------------------- /modules/github/data.tf: -------------------------------------------------------------------------------- 1 | data "github_organization" "alz" { 2 | name = var.organization_name 3 | } 4 | -------------------------------------------------------------------------------- /modules/github/environment.tf: -------------------------------------------------------------------------------- 1 | resource "github_repository_environment" "alz" { 2 | depends_on = [github_team_repository.alz] 3 | for_each = var.environments 4 | environment = each.value 5 | repository = github_repository.alz.name 6 | 7 | dynamic "reviewers" { 8 | for_each = each.key == local.apply_key && length(var.approvers) > 0 ? [1] : [] 9 | content { 10 | teams = [ 11 | github_team.alz.id 12 | ] 13 | } 14 | } 15 | 16 | dynamic "deployment_branch_policy" { 17 | for_each = each.key == local.apply_key ? [1] : [] 18 | content { 19 | protected_branches = true 20 | custom_branch_policies = false 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modules/github/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | apply_key = "apply" 3 | } 4 | 5 | locals { 6 | free_plan = "free" 7 | enterprise_plan = "enterprise" 8 | } 9 | 10 | locals { 11 | use_runner_group = var.use_runner_group && data.github_organization.alz.plan == local.enterprise_plan && var.use_self_hosted_runners 12 | } 13 | 14 | locals { 15 | primary_approver = length(var.approvers) > 0 ? var.approvers[0] : "" 16 | default_commit_email = coalesce(local.primary_approver, "demo@microsoft.com") 17 | } 18 | 19 | locals { 20 | repository_name_templates = var.use_template_repository ? var.repository_name_templates : var.repository_name 21 | template_claim_structure = "${var.organization_name}/${local.repository_name_templates}/%s@refs/heads/main" 22 | 23 | oidc_subjects_flattened = flatten([for key, value in var.workflows : [ 24 | for environment_user_assigned_managed_identity_mapping in value.environment_user_assigned_managed_identity_mappings : 25 | { 26 | subject_key = "${key}-${environment_user_assigned_managed_identity_mapping.user_assigned_managed_identity_key}" 27 | user_assigned_managed_identity_key = environment_user_assigned_managed_identity_mapping.user_assigned_managed_identity_key 28 | subject = "repo:${var.organization_name}/${var.repository_name}:environment:${var.environments[environment_user_assigned_managed_identity_mapping.environment_key]}:job_workflow_ref:${format(local.template_claim_structure, value.workflow_file_name)}" 29 | } 30 | ] 31 | ]) 32 | 33 | oidc_subjects = { for oidc_subject in local.oidc_subjects_flattened : oidc_subject.subject_key => { 34 | user_assigned_managed_identity_key = oidc_subject.user_assigned_managed_identity_key 35 | subject = oidc_subject.subject 36 | } } 37 | } 38 | 39 | locals { 40 | runner_group_name = local.use_runner_group ? github_actions_runner_group.alz[0].name : var.default_runner_group_name 41 | } 42 | -------------------------------------------------------------------------------- /modules/github/oidc_templates.tf: -------------------------------------------------------------------------------- 1 | resource "github_actions_repository_oidc_subject_claim_customization_template" "alz" { 2 | repository = github_repository.alz.name 3 | use_default = false 4 | include_claim_keys = ["repository", "environment", "job_workflow_ref"] 5 | } 6 | -------------------------------------------------------------------------------- /modules/github/outputs.tf: -------------------------------------------------------------------------------- 1 | output "subjects" { 2 | value = local.oidc_subjects 3 | } 4 | 5 | output "issuer" { 6 | value = var.domain_name == "github.com" ? "https://token.actions.githubusercontent.com" : "https://token.actions.${var.domain_name}" 7 | } 8 | 9 | output "organization_users" { 10 | value = data.github_organization.alz.users 11 | } 12 | 13 | output "runner_group_name" { 14 | value = local.runner_group_name 15 | } 16 | 17 | output "organization_plan" { 18 | value = data.github_organization.alz.plan 19 | } 20 | 21 | output "repository_names" { 22 | value = { 23 | module = github_repository.alz.name 24 | templates = var.use_template_repository ? github_repository.alz_templates[0].name : "" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/github/repository_module.tf: -------------------------------------------------------------------------------- 1 | resource "github_repository" "alz" { 2 | name = var.repository_name 3 | description = var.repository_name 4 | auto_init = true 5 | visibility = data.github_organization.alz.plan == local.free_plan ? "public" : "private" 6 | allow_update_branch = true 7 | allow_merge_commit = false 8 | allow_rebase_merge = false 9 | vulnerability_alerts = true 10 | } 11 | 12 | resource "github_repository_file" "alz" { 13 | for_each = var.repository_files 14 | repository = github_repository.alz.name 15 | file = each.key 16 | content = each.value.content 17 | commit_author = local.default_commit_email 18 | commit_email = local.default_commit_email 19 | commit_message = "Add ${each.key} [skip ci]" 20 | overwrite_on_create = true 21 | } 22 | 23 | resource "github_branch_protection" "alz" { 24 | count = var.create_branch_policies ? 1 : 0 25 | depends_on = [github_repository_file.alz] 26 | repository_id = github_repository.alz.name 27 | pattern = "main" 28 | enforce_admins = true 29 | required_linear_history = true 30 | require_conversation_resolution = true 31 | 32 | required_pull_request_reviews { 33 | dismiss_stale_reviews = true 34 | restrict_dismissals = true 35 | required_approving_review_count = length(var.approvers) > 1 ? 1 : 0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/github/repository_templates.tf: -------------------------------------------------------------------------------- 1 | resource "github_repository" "alz_templates" { 2 | count = var.use_template_repository ? 1 : 0 3 | name = var.repository_name_templates 4 | description = var.repository_name_templates 5 | auto_init = true 6 | visibility = data.github_organization.alz.plan == local.free_plan ? "public" : "private" 7 | allow_update_branch = true 8 | allow_merge_commit = false 9 | allow_rebase_merge = false 10 | vulnerability_alerts = true 11 | } 12 | 13 | resource "github_repository_file" "alz_templates" { 14 | for_each = var.use_template_repository ? var.template_repository_files : {} 15 | repository = github_repository.alz_templates[0].name 16 | file = each.key 17 | content = each.value.content 18 | commit_author = local.default_commit_email 19 | commit_email = local.default_commit_email 20 | commit_message = "Add ${each.key} [skip ci]" 21 | overwrite_on_create = true 22 | } 23 | 24 | resource "github_branch_protection" "alz_templates" { 25 | count = var.use_template_repository && var.create_branch_policies ? 1 : 0 26 | depends_on = [github_repository_file.alz_templates] 27 | repository_id = github_repository.alz_templates[0].name 28 | pattern = "main" 29 | enforce_admins = true 30 | required_linear_history = true 31 | require_conversation_resolution = true 32 | 33 | required_pull_request_reviews { 34 | dismiss_stale_reviews = true 35 | restrict_dismissals = true 36 | required_approving_review_count = length(var.approvers) > 1 ? 1 : 0 37 | } 38 | } 39 | 40 | resource "github_actions_repository_access_level" "alz_templates" { 41 | count = var.use_template_repository && data.github_organization.alz.plan == local.enterprise_plan ? 1 : 0 42 | access_level = "organization" 43 | repository = github_repository.alz_templates[0].name 44 | } 45 | -------------------------------------------------------------------------------- /modules/github/runner_group.tf: -------------------------------------------------------------------------------- 1 | resource "github_actions_runner_group" "alz" { 2 | count = local.use_runner_group ? 1 : 0 3 | name = var.runner_group_name 4 | visibility = "selected" 5 | selected_repository_ids = var.use_template_repository ? [github_repository.alz.repo_id, github_repository.alz_templates[0].repo_id] : [github_repository.alz.repo_id] 6 | } 7 | -------------------------------------------------------------------------------- /modules/github/team.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | approvers_by_email = [for approver in var.approvers : approver if strcontains(approver, "@")] 3 | approvers_by_login = [for approver in var.approvers : approver if !strcontains(approver, "@")] 4 | 5 | users_by_email = [for user in data.github_organization.alz.users : { 6 | login = user.login 7 | email = user.email 8 | } if contains(local.approvers_by_email, user.email)] 9 | 10 | users_by_login = [for user in data.github_organization.alz.users : { 11 | login = user.login 12 | email = user.email 13 | } if contains(local.approvers_by_login, user.login)] 14 | 15 | approvers = concat(local.users_by_email, local.users_by_login) 16 | 17 | invalid_approvers_by_email = setsubtract(local.approvers_by_email, local.users_by_email[*].email) 18 | invalid_approvers_by_login = setsubtract(local.approvers_by_login, local.users_by_login[*].login) 19 | 20 | invalid_approvers = setunion(local.invalid_approvers_by_email, local.invalid_approvers_by_login) 21 | } 22 | 23 | resource "github_team" "alz" { 24 | name = var.team_name 25 | description = "Approvers for the Landing Zone Terraform Apply" 26 | privacy = "closed" 27 | 28 | lifecycle { 29 | precondition { 30 | condition = length(local.invalid_approvers) == 0 31 | error_message = "At least one approver has not been supplied with a valid email or username. Invalid approvers by email: ${join(", ", local.invalid_approvers_by_email)}. Invalid approvers by username: ${join(", ", local.invalid_approvers_by_login)}." 32 | } 33 | } 34 | } 35 | 36 | resource "github_team_membership" "alz" { 37 | for_each = { for approver in local.approvers : approver.login => approver } 38 | team_id = github_team.alz.id 39 | username = each.value.login 40 | role = "member" 41 | } 42 | 43 | resource "github_team_repository" "alz" { 44 | team_id = github_team.alz.id 45 | repository = github_repository.alz.name 46 | permission = "push" 47 | } 48 | -------------------------------------------------------------------------------- /modules/github/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | github = { 4 | source = "integrations/github" 5 | version = "~> 6.5" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/github/variables.tf: -------------------------------------------------------------------------------- 1 | variable "domain_name" { 2 | type = string 3 | } 4 | 5 | variable "organization_name" { 6 | type = string 7 | } 8 | 9 | variable "repository_name" { 10 | type = string 11 | } 12 | 13 | variable "repository_files" { 14 | type = map(object({ 15 | content = string 16 | })) 17 | } 18 | 19 | variable "template_repository_files" { 20 | type = map(object({ 21 | content = string 22 | })) 23 | } 24 | 25 | variable "environments" { 26 | type = map(string) 27 | } 28 | 29 | variable "managed_identity_client_ids" { 30 | type = map(string) 31 | } 32 | 33 | variable "azure_tenant_id" { 34 | type = string 35 | } 36 | 37 | variable "azure_subscription_id" { 38 | type = string 39 | } 40 | 41 | variable "backend_azure_resource_group_name" { 42 | type = string 43 | } 44 | 45 | variable "backend_azure_storage_account_name" { 46 | type = string 47 | } 48 | 49 | variable "backend_azure_storage_account_container_name" { 50 | type = string 51 | } 52 | 53 | variable "approvers" { 54 | type = list(string) 55 | } 56 | 57 | variable "team_name" { 58 | type = string 59 | } 60 | 61 | variable "use_template_repository" { 62 | type = bool 63 | } 64 | 65 | variable "repository_name_templates" { 66 | type = string 67 | } 68 | 69 | variable "workflows" { 70 | type = map(object({ 71 | workflow_file_name = string 72 | environment_user_assigned_managed_identity_mappings = list(object({ 73 | environment_key = string 74 | user_assigned_managed_identity_key = string 75 | })) 76 | })) 77 | } 78 | 79 | variable "runner_group_name" { 80 | type = string 81 | } 82 | 83 | variable "default_runner_group_name" { 84 | type = string 85 | } 86 | 87 | variable "use_runner_group" { 88 | type = bool 89 | } 90 | 91 | variable "use_self_hosted_runners" { 92 | type = bool 93 | } 94 | 95 | variable "create_branch_policies" { 96 | type = bool 97 | } 98 | -------------------------------------------------------------------------------- /modules/resource_names/locals.tf: -------------------------------------------------------------------------------- 1 | # Resource Name Setup 2 | resource "random_string" "alz" { 3 | length = 4 4 | special = false 5 | upper = false 6 | numeric = false 7 | } 8 | 9 | locals { 10 | formatted_postfix_number = format("%03d", var.postfix_number) 11 | formatted_postfix_number_plus_1 = format("%03d", var.postfix_number + 1) 12 | formatted_postfix_number_plus_2 = format("%03d", var.postfix_number + 2) 13 | formatted_postfix_number_plus_3 = format("%03d", var.postfix_number + 3) 14 | random_string = random_string.alz.result 15 | resource_names = { 16 | for key, value in var.resource_names : key => replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(value, 17 | "{{service_name}}", var.service_name), 18 | "{{service_name_short}}", substr(var.service_name, 0, 3)), 19 | "{{environment_name}}", var.environment_name), 20 | "{{environment_name_short}}", substr(var.environment_name, 0, 3)), 21 | "{{azure_location}}", var.azure_location), 22 | "{{azure_location_short}}", substr(var.azure_location, 0, 3)), 23 | "{{postfix_number}}", local.formatted_postfix_number), 24 | "{{postfix_number_plus_1}}", local.formatted_postfix_number_plus_1), 25 | "{{postfix_number_plus_2}}", local.formatted_postfix_number_plus_2), 26 | "{{postfix_number_plus_3}}", local.formatted_postfix_number_plus_3), 27 | "{{random_string}}", local.random_string) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/resource_names/outputs.tf: -------------------------------------------------------------------------------- 1 | output "resource_names" { 2 | value = local.resource_names 3 | } 4 | -------------------------------------------------------------------------------- /modules/resource_names/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | random = { 4 | source = "hashicorp/random" 5 | version = "~> 3.5" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/resource_names/variables.tf: -------------------------------------------------------------------------------- 1 | variable "azure_location" { 2 | type = string 3 | } 4 | 5 | variable "environment_name" { 6 | type = string 7 | } 8 | 9 | variable "service_name" { 10 | type = string 11 | } 12 | 13 | variable "postfix_number" { 14 | type = number 15 | } 16 | 17 | variable "resource_names" { 18 | type = map(string) 19 | } 20 | -------------------------------------------------------------------------------- /modules/template_architecture_definition/data.tf: -------------------------------------------------------------------------------- 1 | data "local_file" "architecture_definition_override_json" { 2 | count = local.has_architecture_definition_override ? 1 : 0 3 | filename = var.architecture_definition_override_path 4 | } 5 | -------------------------------------------------------------------------------- /modules/template_architecture_definition/outputs.tf: -------------------------------------------------------------------------------- 1 | output "architecture_definition_json" { 2 | value = local.has_architecture_definition_override ? data.local_file.architecture_definition_override_json[0].content : local.template_file 3 | 4 | precondition { 5 | condition = length(local.management_group_configuration) != 0 6 | error_message = "The management group configuration is required" 7 | } 8 | 9 | precondition { 10 | condition = length(local.management_groups_validation) == 0 11 | error_message = format("Management group ID and display name are required for %s management group(s).", join(", ", local.management_groups_validation)) 12 | } 13 | 14 | precondition { 15 | condition = try([for k, v in local.platform_management_group_children : [v.id, v.display_name]], null) != null 16 | error_message = "Management group ID and display name are required for platform management group children." 17 | } 18 | 19 | precondition { 20 | condition = try([for k, v in local.landing_zone_management_group_children : [v.id, v.display_name]], null) != null 21 | error_message = "Management group ID and display name are required for landing zone management group children." 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modules/template_architecture_definition/templates/architecture_definition.json.tftpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "${architecture_definition_name}", 3 | "management_groups": [ 4 | %{ for management_group in management_groups }{ 5 | "archetypes": ${management_group.archetypes}, 6 | "display_name": "${management_group.display_name}", 7 | "id": "${management_group.id}", 8 | "exists": ${management_group.exists}, 9 | "parent_id": ${management_group.parent_id} 10 | }%{ if management_group != management_groups[length(management_groups) - 1] && length(management_groups) != 0}, 11 | %{ endif }%{ endfor } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /modules/template_architecture_definition/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | local = { 4 | source = "hashicorp/local" 5 | version = "~> 2.4" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/template_architecture_definition/variables.tf: -------------------------------------------------------------------------------- 1 | variable "starter_module_folder_path" { 2 | type = string 3 | description = "Starter module folder path" 4 | } 5 | 6 | variable "architecture_definition_name" { 7 | type = string 8 | description = "Name of the architecture definition" 9 | } 10 | 11 | variable "architecture_definition_template_path" { 12 | type = string 13 | default = "" 14 | description = "The path to the architecture definition template file to use." 15 | } 16 | 17 | variable "architecture_definition_override_path" { 18 | type = string 19 | default = "" 20 | description = "The path to the architecture definition file to use instead of the default." 21 | } 22 | 23 | variable "apply_alz_archetypes_via_architecture_definition_template" { 24 | type = bool 25 | default = true 26 | description = "Toggles assignment of ALZ policies. True to deploy, otherwise false. (e.g true)" 27 | } 28 | --------------------------------------------------------------------------------