diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index b4171ba5..da6dff58 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -73,6 +73,7 @@ jobs: $Inputs["starter_module"] = ".test" $Inputs["version_control_system_access_token"] = if ($versionControlSystem -eq "github") { "${{ secrets.VCS_TOKEN_GITHUB }}" } else { "${{ secrets.VCS_TOKEN_AZURE_DEVOPS }}" } $Inputs["version_control_system_organization"] = "${{ vars.VCS_ORGANIZATION }}" + $Inputs["version_control_system_use_separate_repository_for_templates"] = "true" $Inputs["azure_location"] = "uksouth" $Inputs["azure_subscription_id"] = "" $Inputs["service_name"] = "alz" diff --git a/bootstrap/azuredevops/data.tf b/bootstrap/azuredevops/data.tf new file mode 100644 index 00000000..df00b8a0 --- /dev/null +++ b/bootstrap/azuredevops/data.tf @@ -0,0 +1,2 @@ +data "azurerm_client_config" "current" {} +data "azurerm_subscription" "current" {} diff --git a/bootstrap/azuredevops/files.tf b/bootstrap/azuredevops/files.tf new file mode 100644 index 00000000..d8d94058 --- /dev/null +++ b/bootstrap/azuredevops/files.tf @@ -0,0 +1,39 @@ +locals { + starter_module_folder_path = var.module_folder_path_relative ? ("${path.module}/${var.module_folder_path}/${var.starter_module}") : "${var.module_folder_path}/${var.starter_module}" + pipeline_folder_path = var.pipeline_folder_path_relative ? ("${path.module}/${var.pipeline_folder_path}") : var.pipeline_folder_path +} + +locals { + file_type_flags = { + pipeline = "pipeline" + pipeline_template = "pipeline_template" + module = "module" + additional = "additional" + } +} + +module "starter_module_files" { + source = "./../modules/files" + folder_path = local.starter_module_folder_path + flag = local.file_type_flags.module +} + +locals { + pipeline_files = { for key, value in var.pipeline_files : value.target_path => { + path = "${local.pipeline_folder_path}/${value.file_path}" + flag = local.file_type_flags.pipeline + } + } + template_files = { for key, value in var.pipeline_template_files : value.target_path => { + path = "${local.pipeline_folder_path}/${value.file_path}" + flag = local.file_type_flags.pipeline_template + } + } + starter_module_repo_files = merge(module.starter_module_files.files, local.pipeline_files, local.template_files) + additional_repo_files = { for file in var.additional_files : basename(file) => { + path = file + flag = local.file_type_flags.additional + } + } + all_repo_files = merge(local.starter_module_repo_files, local.additional_repo_files) +} diff --git a/bootstrap/azuredevops/locals.tf b/bootstrap/azuredevops/locals.tf index 9d320e89..bbc2b156 100644 --- a/bootstrap/azuredevops/locals.tf +++ b/bootstrap/azuredevops/locals.tf @@ -7,3 +7,79 @@ locals { plan_key = "plan" apply_key = "apply" } + +locals { + ci_key = "ci" + cd_key = "cd" +} + +locals { + managed_identities = { + (local.plan_key) = local.resource_names.user_assigned_managed_identity_plan + (local.apply_key) = local.resource_names.user_assigned_managed_identity_apply + } + + federated_credentials = module.azure_devops.is_authentication_scheme_workload_identity_federation ? { + (local.plan_key) = { + user_assigned_managed_identity_key = local.plan_key + federated_credential_subject = module.azure_devops.is_authentication_scheme_workload_identity_federation ? module.azure_devops.subjects[local.plan_key] : "" + federated_credential_issuer = module.azure_devops.is_authentication_scheme_workload_identity_federation ? module.azure_devops.issuers[local.plan_key] : "" + federated_credential_name = local.resource_names.user_assigned_managed_identity_federated_credentials_plan + } + (local.apply_key) = { + user_assigned_managed_identity_key = local.apply_key + federated_credential_subject = module.azure_devops.is_authentication_scheme_workload_identity_federation ? module.azure_devops.subjects[local.apply_key] : "" + federated_credential_issuer = module.azure_devops.is_authentication_scheme_workload_identity_federation ? module.azure_devops.issuers[local.apply_key] : "" + federated_credential_name = local.resource_names.user_assigned_managed_identity_federated_credentials_apply + } + } : {} + + agent_container_instances = module.azure_devops.is_authentication_scheme_managed_identity ? { + agent_01 = { + container_instance_name = local.resource_names.container_instance_01 + agent_name = local.resource_names.agent_01 + managed_identity_key = local.plan_key + agent_pool_name = module.azure_devops.is_authentication_scheme_managed_identity ? module.azure_devops.agent_pool_names[local.plan_key] : "" + } + agent_02 = { + container_instance_name = local.resource_names.container_instance_02 + agent_name = local.resource_names.agent_02 + managed_identity_key = local.plan_key + agent_pool_name = module.azure_devops.is_authentication_scheme_managed_identity ? module.azure_devops.agent_pool_names[local.plan_key] : "" + } + agent_03 = { + container_instance_name = local.resource_names.container_instance_03 + agent_name = local.resource_names.agent_03 + managed_identity_key = local.apply_key + agent_pool_name = module.azure_devops.is_authentication_scheme_managed_identity ? module.azure_devops.agent_pool_names[local.apply_key] : "" + } + agent_04 = { + container_instance_name = local.resource_names.container_instance_04 + agent_name = local.resource_names.agent_04 + managed_identity_key = local.apply_key + agent_pool_name = module.azure_devops.is_authentication_scheme_managed_identity ? module.azure_devops.agent_pool_names[local.apply_key] : "" + } + } : {} +} + +locals { + environments = { + (local.plan_key) = { + environment_name = local.resource_names.version_control_system_environment_plan + service_connection_name = local.resource_names.version_control_system_service_connection_plan + service_connection_template_keys = [ + local.ci_key, + local.cd_key + ] + agent_pool_name = local.resource_names.version_control_system_agent_pool_plan + } + (local.apply_key) = { + environment_name = local.resource_names.version_control_system_environment_apply + service_connection_name = local.resource_names.version_control_system_service_connection_apply + service_connection_template_keys = [ + local.cd_key + ] + agent_pool_name = local.resource_names.version_control_system_agent_pool_apply + } + } +} diff --git a/bootstrap/azuredevops/main.tf b/bootstrap/azuredevops/main.tf index df20ff72..c06286cf 100644 --- a/bootstrap/azuredevops/main.tf +++ b/bootstrap/azuredevops/main.tf @@ -1,6 +1,3 @@ -data "azurerm_client_config" "current" {} -data "azurerm_subscription" "current" {} - module "resource_names" { source = "./../modules/resource_names" azure_location = var.azure_location @@ -10,53 +7,6 @@ module "resource_names" { resource_names = var.resource_names } -locals { - managed_identities = { - (local.plan_key) = local.resource_names.user_assigned_managed_identity_plan - (local.apply_key) = local.resource_names.user_assigned_managed_identity_apply - } - - federated_credentials = module.azure_devops.is_authentication_scheme_workload_identity_federation ? { - (local.plan_key) = { - federated_credential_subject = module.azure_devops.is_authentication_scheme_workload_identity_federation ? module.azure_devops.subjects[local.plan_key] : "" - federated_credential_issuer = module.azure_devops.is_authentication_scheme_workload_identity_federation ? module.azure_devops.issuers[local.plan_key] : "" - federated_credential_name = local.resource_names.user_assigned_managed_identity_federated_credentials_plan - } - (local.apply_key) = { - federated_credential_subject = module.azure_devops.is_authentication_scheme_workload_identity_federation ? module.azure_devops.subjects[local.apply_key] : "" - federated_credential_issuer = module.azure_devops.is_authentication_scheme_workload_identity_federation ? module.azure_devops.issuers[local.apply_key] : "" - federated_credential_name = local.resource_names.user_assigned_managed_identity_federated_credentials_apply - } - } : {} - - agent_container_instances = module.azure_devops.is_authentication_scheme_managed_identity ? { - agent_01 = { - container_instance_name = local.resource_names.container_instance_01 - agent_name = local.resource_names.agent_01 - managed_identity_key = local.plan_key - agent_pool_name = module.azure_devops.is_authentication_scheme_managed_identity ? module.azure_devops.agent_pool_names[local.plan_key] : "" - } - agent_02 = { - container_instance_name = local.resource_names.container_instance_02 - agent_name = local.resource_names.agent_02 - managed_identity_key = local.plan_key - agent_pool_name = module.azure_devops.is_authentication_scheme_managed_identity ? module.azure_devops.agent_pool_names[local.plan_key] : "" - } - agent_03 = { - container_instance_name = local.resource_names.container_instance_03 - agent_name = local.resource_names.agent_03 - managed_identity_key = local.apply_key - agent_pool_name = module.azure_devops.is_authentication_scheme_managed_identity ? module.azure_devops.agent_pool_names[local.apply_key] : "" - } - agent_04 = { - container_instance_name = local.resource_names.container_instance_04 - agent_name = local.resource_names.agent_04 - managed_identity_key = local.apply_key - agent_pool_name = module.azure_devops.is_authentication_scheme_managed_identity ? module.azure_devops.agent_pool_names[local.apply_key] : "" - } - } : {} -} - module "azure" { source = "./../modules/azure" create_agents_resource_group = module.azure_devops.is_authentication_scheme_managed_identity @@ -78,49 +28,6 @@ module "azure" { root_management_group_display_name = var.root_management_group_display_name } -locals { - starter_module_path = abspath("${path.module}/${var.template_folder_path}/${var.starter_module}") - ci_cd_module_path = abspath("${path.module}/${var.template_folder_path}/${var.ci_cd_module}") -} - -module "starter_module_files" { - source = "./../modules/files" - folder_path = local.starter_module_path - flag = "module" -} - -module "ci_cd_module_files" { - source = "./../modules/files" - folder_path = local.ci_cd_module_path - exclusions = [".github"] - flag = "cicd" -} - -locals { - starter_module_repo_files = merge(module.starter_module_files.files, module.ci_cd_module_files.files) - additional_repo_files = { for file in var.additional_files : basename(file) => { - path = file - flag = "additional" - } - } - all_repo_files = merge(local.starter_module_repo_files, local.additional_repo_files) -} - -locals { - environments = { - (local.plan_key) = { - environment_name = local.resource_names.version_control_system_environment_plan - service_connection_name = local.resource_names.version_control_system_service_connection_plan - agent_pool_name = local.resource_names.version_control_system_agent_pool_plan - } - (local.apply_key) = { - environment_name = local.resource_names.version_control_system_environment_apply - service_connection_name = local.resource_names.version_control_system_service_connection_apply - agent_pool_name = local.resource_names.version_control_system_agent_pool_apply - } - } -} - module "azure_devops" { source = "./../modules/azure_devops" use_legacy_organization_url = var.azure_devops_use_organisation_legacy_url @@ -132,12 +39,14 @@ module "azure_devops" { managed_identity_client_ids = module.azure.user_assigned_managed_identity_client_ids repository_name = local.resource_names.version_control_system_repository repository_files = local.all_repo_files + use_template_repository = var.version_control_system_use_separate_repository_for_templates + repository_name_templates = local.resource_names.version_control_system_repository_templates variable_group_name = local.resource_names.version_control_system_variable_group azure_tenant_id = data.azurerm_client_config.current.tenant_id azure_subscription_id = data.azurerm_client_config.current.subscription_id azure_subscription_name = data.azurerm_subscription.current.display_name - pipeline_ci_file = var.ci_file_path - pipeline_cd_file = var.cd_file_path + pipelines = var.pipeline_files + pipeline_templates = var.pipeline_template_files backend_azure_resource_group_name = local.resource_names.resource_group_state backend_azure_storage_account_name = local.resource_names.storage_account backend_azure_storage_account_container_name = local.resource_names.storage_container diff --git a/bootstrap/azuredevops/terraform.tfvars b/bootstrap/azuredevops/terraform.tfvars index a6871286..af82e9af 100644 --- a/bootstrap/azuredevops/terraform.tfvars +++ b/bootstrap/azuredevops/terraform.tfvars @@ -1,9 +1,3 @@ -# Version Control System Variables -template_folder_path = "../../templates" -ci_cd_module = ".ci_cd" -ci_file_path = ".azuredevops/ci.yaml" -cd_file_path = ".azuredevops/cd.yaml" - # Azure Variables agent_container_image = "jaredfholgate/azure-devops-agent:0.0.3" @@ -27,6 +21,7 @@ resource_names = { agent_03 = "agent-{{service_name}}-{{environment_name}}-{{postfix_number_plus_2}}" agent_04 = "agent-{{service_name}}-{{environment_name}}-{{postfix_number_plus_3}}" version_control_system_repository = "{{service_name}}-{{environment_name}}" + version_control_system_repository_templates = "{{service_name}}-{{environment_name}}-templates" version_control_system_service_connection_plan = "sc-{{service_name}}-{{environment_name}}-plan" version_control_system_service_connection_apply = "sc-{{service_name}}-{{environment_name}}-apply" version_control_system_environment_plan = "{{service_name}}-{{environment_name}}-plan" @@ -36,3 +31,59 @@ resource_names = { version_control_system_agent_pool_apply = "{{service_name}}-{{environment_name}}-apply" version_control_system_group = "{{service_name}}-{{environment_name}}-approvers" } + +# Version Control System Variables +module_folder_path = "../../templates" +pipeline_folder_path = "../../templates/ci_cd" + +pipeline_files = { + ci = { + pipeline_name = "01 Azure Landing Zone Continuous Integration" + file_path = "azuredevops/ci.yaml" + target_path = ".pipelines/ci.yaml" + environment_keys = [ + "plan" + ] + service_connection_keys = [ + "plan" + ] + agent_pool_keys = [ + "plan" + ] + } + cd = { + pipeline_name = "02 Azure Landing Zone Continuous Delivery" + file_path = "azuredevops/cd.yaml" + target_path = ".pipelines/cd.yaml" + environment_keys = [ + "plan", + "apply" + ] + service_connection_keys = [ + "plan", + "apply" + ] + agent_pool_keys = [ + "plan", + "apply" + ] + } +} +pipeline_template_files = { + plan = { + file_path = "azuredevops/templates/plan.yaml" + target_path = "plan.yaml" + } + apply = { + file_path = "azuredevops/templates/apply.yaml" + target_path = "apply.yaml" + } + ci = { + file_path = "azuredevops/templates/ci.yaml" + target_path = "ci.yaml" + } + cd = { + file_path = "azuredevops/templates/cd.yaml" + target_path = "cd.yaml" + } +} diff --git a/bootstrap/azuredevops/variables.tf b/bootstrap/azuredevops/variables.tf index 501cacda..a19e42a8 100644 --- a/bootstrap/azuredevops/variables.tf +++ b/bootstrap/azuredevops/variables.tf @@ -15,55 +15,61 @@ variable "version_control_system_organization" { type = string } +variable "version_control_system_use_separate_repository_for_templates" { + description = "Controls whether to use a separate repository to store pipeline templates. This is an extra layer of security to ensure that the azure credentials can only be leveraged for the specified workload|4" + type = bool + default = true +} + variable "azure_location" { - description = "Azure Deployment location for the landing zone management resources|4|azure_location" + description = "Azure Deployment location for the landing zone management resources|5|azure_location" type = string } variable "azure_subscription_id" { - description = "Azure Subscription ID for the landing zone management resources. Leave empty to use the az login subscription|5|azure_subscription_id" + description = "Azure Subscription ID for the landing zone management resources. Leave empty to use the az login subscription|6|azure_subscription_id" type = string default = "" } variable "service_name" { - description = "Used to build up the default resource names (e.g. rg--mgmt-uksouth-001)|6|azure_name_section" + description = "Used to build up the default resource names (e.g. rg--mgmt-uksouth-001)|7|azure_name_section" type = string default = "alz" } variable "environment_name" { - description = "Used to build up the default resource names (e.g. rg-alz--uksouth-001)|7|azure_name_section" + description = "Used to build up the default resource names (e.g. rg-alz--uksouth-001)|8|azure_name_section" type = string default = "mgmt" } variable "postfix_number" { - description = "Used to build up the default resource names (e.g. rg-alz-mgmt-uksouth-)|8|number" + description = "Used to build up the default resource names (e.g. rg-alz-mgmt-uksouth-)|9|number" type = number default = 1 } variable "azure_devops_use_organisation_legacy_url" { - description = "Use the legacy Azure DevOps URL (.visualstudio.com) instead of the new URL (dev.azure.com/). This is ignored if an fqdn is supplied for version_control_system_organization|9|bool" + description = "Use the legacy Azure DevOps URL (.visualstudio.com) instead of the new URL (dev.azure.com/). This is ignored if an fqdn is supplied for version_control_system_organization|10|bool" type = bool default = false } variable "azure_devops_create_project" { - description = "Create the Azure DevOps project if it does not exist|10|bool" + description = "Create the Azure DevOps project if it does not exist|11|bool" type = bool default = true } variable "azure_devops_project_name" { - description = "The name of the Azure DevOps project to use or create for the deployment|11" + description = "The name of the Azure DevOps project to use or create for the deployment|12" type = string } variable "azure_devops_authentication_scheme" { type = string - description = "The authentication scheme to use for the Azure DevOps Pipelines|12|auth_scheme" + description = "The authentication scheme to use for the Azure DevOps Pipelines|13|auth_scheme" validation { condition = can(regex("^(ManagedServiceIdentity|WorkloadIdentityFederation)$", var.azure_devops_authentication_scheme)) error_message = "azure_devops_authentication_scheme must be either ManagedServiceIdentity or WorkloadIdentityFederation" @@ -72,19 +78,19 @@ variable "azure_devops_authentication_scheme" { } variable "apply_approvers" { - description = "Apply stage approvers to the action / pipeline, must be a list of SPNs separate by a comma (e.g. abcdef@microsoft.com,ghijklm@microsoft.com)|13" + description = "Apply stage approvers to the action / pipeline, must be a list of SPNs separate by a comma (e.g. abcdef@microsoft.com,ghijklm@microsoft.com)|14" type = list(string) default = [] } variable "root_management_group_display_name" { - description = "The root management group display name|14" + description = "The root management group display name|15" type = string default = "Tenant Root Group" } variable "additional_files" { - description = "Additional files to upload to the repository. This must be specified as a comma-separated list of absolute file paths (e.g. c:\\config\\config.yaml or /home/user/config/config.yaml)|15" + description = "Additional files to upload to the repository. This must be specified as a comma-separated list of absolute file paths (e.g. c:\\config\\config.yaml or /home/user/config/config.yaml)|16" type = list(string) default = [] } @@ -99,28 +105,49 @@ variable "target_subscriptions" { type = list(string) } -variable "template_folder_path" { - description = "The folder for the templates|hidden" +variable "module_folder_path" { + description = "The folder for the starter modules|hidden" type = string } -variable "ci_cd_module" { - description = "The folder for the ci/cd module|hidden" - type = string +variable "module_folder_path_relative" { + description = "Whether the module folder path is relative to the bootstrap module|hidden" + type = bool + default = true } -variable "ci_file_path" { - description = "The path to the ci file (e.g. ci.yaml)|hidden" +variable "pipeline_folder_path" { + description = "The folder for the pipelines|hidden" type = string } -variable "cd_file_path" { - description = "The path to the cd file (e.g. cd.yaml)|hidden" - type = string +variable "pipeline_folder_path_relative" { + description = "Whether the pipeline folder path is relative to the bootstrap module|hidden" + type = bool + default = true +} + +variable "pipeline_files" { + description = "The pipeline files to upload to the repository|hidden" + type = map(object({ + pipeline_name = string + file_path = string + target_path = string + environment_keys = list(string) + service_connection_keys = list(string) + agent_pool_keys = list(string) + })) +} + +variable "pipeline_template_files" { + description = "The pipeline template files to upload to the repository|hidden" + type = map(object({ + file_path = string + target_path = string + })) } variable "resource_names" { type = map(string) description = "Overrides for resource names|hidden" } - diff --git a/bootstrap/github/data.tf b/bootstrap/github/data.tf new file mode 100644 index 00000000..cee07df2 --- /dev/null +++ b/bootstrap/github/data.tf @@ -0,0 +1 @@ +data "azurerm_client_config" "current" {} diff --git a/bootstrap/github/files.tf b/bootstrap/github/files.tf new file mode 100644 index 00000000..d8d94058 --- /dev/null +++ b/bootstrap/github/files.tf @@ -0,0 +1,39 @@ +locals { + starter_module_folder_path = var.module_folder_path_relative ? ("${path.module}/${var.module_folder_path}/${var.starter_module}") : "${var.module_folder_path}/${var.starter_module}" + pipeline_folder_path = var.pipeline_folder_path_relative ? ("${path.module}/${var.pipeline_folder_path}") : var.pipeline_folder_path +} + +locals { + file_type_flags = { + pipeline = "pipeline" + pipeline_template = "pipeline_template" + module = "module" + additional = "additional" + } +} + +module "starter_module_files" { + source = "./../modules/files" + folder_path = local.starter_module_folder_path + flag = local.file_type_flags.module +} + +locals { + pipeline_files = { for key, value in var.pipeline_files : value.target_path => { + path = "${local.pipeline_folder_path}/${value.file_path}" + flag = local.file_type_flags.pipeline + } + } + template_files = { for key, value in var.pipeline_template_files : value.target_path => { + path = "${local.pipeline_folder_path}/${value.file_path}" + flag = local.file_type_flags.pipeline_template + } + } + starter_module_repo_files = merge(module.starter_module_files.files, local.pipeline_files, local.template_files) + additional_repo_files = { for file in var.additional_files : basename(file) => { + path = file + flag = local.file_type_flags.additional + } + } + all_repo_files = merge(local.starter_module_repo_files, local.additional_repo_files) +} diff --git a/bootstrap/github/locals.tf b/bootstrap/github/locals.tf index 9d320e89..56f0dfab 100644 --- a/bootstrap/github/locals.tf +++ b/bootstrap/github/locals.tf @@ -7,3 +7,26 @@ locals { plan_key = "plan" apply_key = "apply" } + +locals { + environments = { + (local.plan_key) = local.resource_names.version_control_system_environment_plan + (local.apply_key) = local.resource_names.version_control_system_environment_apply + } +} + +locals { + managed_identities = { + (local.plan_key) = local.resource_names.user_assigned_managed_identity_plan + (local.apply_key) = local.resource_names.user_assigned_managed_identity_apply + } + + federated_credentials = { for key, value in module.github.subjects : + key => { + user_assigned_managed_identity_key = value.user_assigned_managed_identity_key + federated_credential_subject = value.subject + federated_credential_issuer = module.github.issuer + federated_credential_name = "${local.resource_names.user_assigned_managed_identity_federated_credentials_prefix}-${key}" + } + } +} diff --git a/bootstrap/github/main.tf b/bootstrap/github/main.tf index 7d07b85a..b1b82596 100644 --- a/bootstrap/github/main.tf +++ b/bootstrap/github/main.tf @@ -1,5 +1,3 @@ -data "azurerm_client_config" "current" {} - module "resource_names" { source = "./../modules/resource_names" azure_location = var.azure_location @@ -9,26 +7,6 @@ module "resource_names" { resource_names = var.resource_names } -locals { - managed_identities = { - (local.plan_key) = local.resource_names.user_assigned_managed_identity_plan - (local.apply_key) = local.resource_names.user_assigned_managed_identity_apply - } - - federated_credentials = { - (local.plan_key) = { - federated_credential_subject = module.github.subjects[local.plan_key] - federated_credential_issuer = module.github.issuer - federated_credential_name = local.resource_names.user_assigned_managed_identity_federated_credentials_plan - } - (local.apply_key) = { - federated_credential_subject = module.github.subjects[local.apply_key] - federated_credential_issuer = module.github.issuer - federated_credential_name = local.resource_names.user_assigned_managed_identity_federated_credentials_apply - } - } -} - module "azure" { source = "./../modules/azure" user_assigned_managed_identities = local.managed_identities @@ -42,48 +20,16 @@ module "azure" { root_management_group_display_name = var.root_management_group_display_name } -locals { - starter_module_path = abspath("${path.module}/${var.template_folder_path}/${var.starter_module}") - ci_cd_module_path = abspath("${path.module}/${var.template_folder_path}/${var.ci_cd_module}") -} - -module "starter_module_files" { - source = "./../modules/files" - folder_path = local.starter_module_path - flag = "module" -} - -module "ci_cd_module_files" { - source = "./../modules/files" - folder_path = local.ci_cd_module_path - exclusions = [".azuredevops"] - flag = "cicd" -} - -locals { - starter_module_repo_files = merge(module.starter_module_files.files, module.ci_cd_module_files.files) - additional_repo_files = { for file in var.additional_files : basename(file) => { - path = file - flag = "additional" - } - } - all_repo_files = merge(local.starter_module_repo_files, local.additional_repo_files) -} - -locals { - environments = { - (local.plan_key) = local.resource_names.version_control_system_environment_plan - (local.apply_key) = local.resource_names.version_control_system_environment_apply - } -} - module "github" { source = "./../modules/github" organization_name = var.version_control_system_organization environments = local.environments repository_name = local.resource_names.version_control_system_repository + use_template_repository = var.version_control_system_use_separate_repository_for_templates + repository_name_templates = local.resource_names.version_control_system_repository_templates repository_visibility = var.repository_visibility repository_files = local.all_repo_files + pipeline_templates = var.pipeline_template_files managed_identity_client_ids = module.azure.user_assigned_managed_identity_client_ids azure_tenant_id = data.azurerm_client_config.current.tenant_id azure_subscription_id = data.azurerm_client_config.current.subscription_id diff --git a/bootstrap/github/terraform.tfvars b/bootstrap/github/terraform.tfvars index 06510d5b..ca8cb25a 100644 --- a/bootstrap/github/terraform.tfvars +++ b/bootstrap/github/terraform.tfvars @@ -1,19 +1,51 @@ -# Version Control System Variables -template_folder_path = "../../templates" -ci_cd_module = ".ci_cd" - # Naming resource_names = { - resource_group_state = "rg-{{service_name}}-{{environment_name}}-state-{{azure_location}}-{{postfix_number}}" - resource_group_identity = "rg-{{service_name}}-{{environment_name}}-identity-{{azure_location}}-{{postfix_number}}" - user_assigned_managed_identity_plan = "id-{{service_name}}-{{environment_name}}-{{azure_location}}-plan-{{postfix_number}}" - user_assigned_managed_identity_apply = "id-{{service_name}}-{{environment_name}}-{{azure_location}}-apply-{{postfix_number}}" - user_assigned_managed_identity_federated_credentials_plan = "id-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}-plan" - user_assigned_managed_identity_federated_credentials_apply = "id-{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}-apply" - storage_account = "sto{{service_name}}{{environment_name}}{{azure_location_short}}{{postfix_number}}{{random_string}}" - storage_container = "{{environment_name}}-tfstate" - version_control_system_repository = "{{service_name}}-{{environment_name}}" - version_control_system_environment_plan = "{{service_name}}-{{environment_name}}-plan" - version_control_system_environment_apply = "{{service_name}}-{{environment_name}}-apply" - version_control_system_team = "{{service_name}}-{{environment_name}}-approvers" + resource_group_state = "rg-{{service_name}}-{{environment_name}}-state-{{azure_location}}-{{postfix_number}}" + resource_group_identity = "rg-{{service_name}}-{{environment_name}}-identity-{{azure_location}}-{{postfix_number}}" + user_assigned_managed_identity_plan = "id-{{service_name}}-{{environment_name}}-{{azure_location}}-plan-{{postfix_number}}" + user_assigned_managed_identity_apply = "id-{{service_name}}-{{environment_name}}-{{azure_location}}-apply-{{postfix_number}}" + user_assigned_managed_identity_federated_credentials_prefix = "{{service_name}}-{{environment_name}}-{{azure_location}}-{{postfix_number}}" + storage_account = "sto{{service_name}}{{environment_name}}{{azure_location_short}}{{postfix_number}}{{random_string}}" + storage_container = "{{environment_name}}-tfstate" + version_control_system_repository = "{{service_name}}-{{environment_name}}" + version_control_system_repository_templates = "{{service_name}}-{{environment_name}}-templates" + version_control_system_environment_plan = "{{service_name}}-{{environment_name}}-plan" + version_control_system_environment_apply = "{{service_name}}-{{environment_name}}-apply" + version_control_system_team = "{{service_name}}-{{environment_name}}-approvers" +} + +# Version Control System Variables +module_folder_path = "../../templates" +pipeline_folder_path = "../../templates/ci_cd" +pipeline_files = { + ci = { + file_path = "github/ci.yaml" + target_path = ".github/workflows/ci.yaml" + } + cd = { + file_path = "github/cd.yaml" + target_path = ".github/workflows/cd.yaml" + } +} +pipeline_template_files = { + ci = { + file_path = "github/templates/ci.yaml" + target_path = ".github/workflows/ci_template.yaml" + environment_user_assigned_managed_identity_mappings = [{ + environment_key = "plan" + user_assigned_managed_identity_key = "plan" + }] + } + cd = { + file_path = "github/templates/cd.yaml" + target_path = ".github/workflows/cd_template.yaml" + environment_user_assigned_managed_identity_mappings = [{ + environment_key = "plan" + user_assigned_managed_identity_key = "plan" + }, + { + environment_key = "apply" + user_assigned_managed_identity_key = "apply" + }] + } } diff --git a/bootstrap/github/variables.tf b/bootstrap/github/variables.tf index ec2f35d9..191e3102 100644 --- a/bootstrap/github/variables.tf +++ b/bootstrap/github/variables.tf @@ -15,55 +15,61 @@ variable "version_control_system_organization" { type = string } +variable "version_control_system_use_separate_repository_for_templates" { + description = "Controls whether to use a separate repository to store action templates. This is an extra layer of security to ensure that the azure credentials can only be leveraged for the specified workload|4" + type = bool + default = true +} + variable "azure_location" { - description = "Azure Deployment location for the landing zone management resources|4|azure_location" + description = "Azure Deployment location for the landing zone management resources|5|azure_location" type = string } variable "azure_subscription_id" { - description = "Azure Subscription ID for the landing zone management resources. Leave empty to use the az login subscription|5|azure_subscription_id" + description = "Azure Subscription ID for the landing zone management resources. Leave empty to use the az login subscription|6|azure_subscription_id" type = string default = "" } variable "service_name" { - description = "Used to build up the default resource names (e.g. rg--mgmt-uksouth-001)|6|azure_name_section" + description = "Used to build up the default resource names (e.g. rg--mgmt-uksouth-001)|7|azure_name_section" type = string default = "alz" } variable "environment_name" { - description = "Used to build up the default resource names (e.g. rg-alz--uksouth-001)|7|azure_name_section" + description = "Used to build up the default resource names (e.g. rg-alz--uksouth-001)|8|azure_name_section" type = string default = "mgmt" } variable "postfix_number" { - description = "Used to build up the default resource names (e.g. rg-alz-mgmt-uksouth-)|8|number" + description = "Used to build up the default resource names (e.g. rg-alz-mgmt-uksouth-)|9|number" type = number default = 1 } variable "apply_approvers" { - description = "Apply stage approvers to the action / pipeline, must be a list of SPNs separate by a comma (e.g. abcdef@microsoft.com,ghijklm@microsoft.com)|9" + description = "Apply stage approvers to the action / pipeline, must be a list of SPNs separate by a comma (e.g. abcdef@microsoft.com,ghijklm@microsoft.com)|10" type = list(string) default = [] } variable "repository_visibility" { - description = "The visibility of the repository. Must be 'public' if your organization is not licensed|10|repo_visibility" + description = "The visibility of the repository. Must be 'public' if your organization is not licensed|11|repo_visibility" type = string default = "private" } variable "root_management_group_display_name" { - description = "The root management group display name|11" + description = "The root management group display name|12" type = string default = "Tenant Root Group" } variable "additional_files" { - description = "Additional files to upload to the repository. This must be specified as a comma-separated list of absolute file paths (e.g. c:\\config\\config.yaml or /home/user/config/config.yaml)|12" + description = "Additional files to upload to the repository. This must be specified as a comma-separated list of absolute file paths (e.g. c:\\config\\config.yaml or /home/user/config/config.yaml)|13" type = list(string) default = [] } @@ -73,16 +79,48 @@ variable "target_subscriptions" { type = list(string) } -variable "template_folder_path" { - description = "The folder for the templates|hidden" +variable "module_folder_path" { + description = "The folder for the starter modules|hidden" type = string } -variable "ci_cd_module" { - description = "The folder for the ci/cd module|hidden" +variable "module_folder_path_relative" { + description = "Whether the module folder path is relative to the bootstrap module|hidden" + type = bool + default = true +} + +variable "pipeline_folder_path" { + description = "The folder for the pipelines|hidden" type = string } +variable "pipeline_folder_path_relative" { + description = "Whether the pipeline folder path is relative to the bootstrap module|hidden" + type = bool + default = true +} + +variable "pipeline_files" { + description = "The pipeline files to upload to the repository|hidden" + type = map(object({ + file_path = string + target_path = string + })) +} + +variable "pipeline_template_files" { + description = "The pipeline template files to upload to the repository|hidden" + type = map(object({ + file_path = string + target_path = string + environment_user_assigned_managed_identity_mappings = list(object({ + environment_key = string + user_assigned_managed_identity_key = string + })) + })) +} + variable "resource_names" { type = map(string) description = "Overrides for resource names|hidden" diff --git a/bootstrap/modules/azure/managed_identity.tf b/bootstrap/modules/azure/managed_identity.tf index d31ab47f..b03828c3 100644 --- a/bootstrap/modules/azure/managed_identity.tf +++ b/bootstrap/modules/azure/managed_identity.tf @@ -15,7 +15,7 @@ resource "azurerm_federated_identity_credential" "alz" { resource_group_name = azurerm_resource_group.identity.name audience = [local.audience] issuer = each.value.federated_credential_issuer - parent_id = azurerm_user_assigned_identity.alz[each.key].id + parent_id = azurerm_user_assigned_identity.alz[each.value.user_assigned_managed_identity_key].id subject = each.value.federated_credential_subject } diff --git a/bootstrap/modules/azure/variables.tf b/bootstrap/modules/azure/variables.tf index 5b282af9..b2f5ff5d 100644 --- a/bootstrap/modules/azure/variables.tf +++ b/bootstrap/modules/azure/variables.tf @@ -8,9 +8,10 @@ variable "user_assigned_managed_identities" { variable "federated_credentials" { type = map(object({ - federated_credential_subject = string - federated_credential_issuer = string - federated_credential_name = string + user_assigned_managed_identity_key = string + federated_credential_subject = string + federated_credential_issuer = string + federated_credential_name = string })) } diff --git a/bootstrap/modules/azure_devops/environment.tf b/bootstrap/modules/azure_devops/environment.tf index 07f63948..72bb44f8 100644 --- a/bootstrap/modules/azure_devops/environment.tf +++ b/bootstrap/modules/azure_devops/environment.tf @@ -3,25 +3,3 @@ resource "azuredevops_environment" "alz" { name = each.value.environment_name project_id = local.project_id } - -resource "azuredevops_check_approval" "alz" { - count = length(var.approvers) == 0 ? 0 : 1 - project_id = local.project_id - target_resource_id = azuredevops_environment.alz[local.apply_key].id - target_resource_type = "environment" - - requester_can_approve = length(var.approvers) == 1 - approvers = [ - azuredevops_group.alz_approvers.origin_id - ] - - timeout = 43200 -} - -resource "azuredevops_check_exclusive_lock" "alz" { - for_each = var.environments - project_id = local.project_id - target_resource_id = azuredevops_environment.alz[each.key].id - target_resource_type = "environment" - timeout = 43200 -} diff --git a/bootstrap/modules/azure_devops/locals.tf b/bootstrap/modules/azure_devops/locals.tf index e6e2b10b..99673c67 100644 --- a/bootstrap/modules/azure_devops/locals.tf +++ b/bootstrap/modules/azure_devops/locals.tf @@ -13,3 +13,11 @@ locals { is_authentication_scheme_managed_identity = var.authentication_scheme == local.authentication_scheme_managed_identity is_authentication_scheme_workload_identity_federation = var.authentication_scheme == local.authentication_scheme_workload_identity_federation } + +locals { + default_branch = "refs/heads/main" +} + +locals { + repository_name_templates = var.use_template_repository ? var.repository_name_templates : var.repository_name +} diff --git a/bootstrap/modules/azure_devops/locals_files.tf b/bootstrap/modules/azure_devops/locals_files.tf new file mode 100644 index 00000000..6d08cf66 --- /dev/null +++ b/bootstrap/modules/azure_devops/locals_files.tf @@ -0,0 +1,40 @@ +locals { + agent_pool_configuration_plan = local.is_authentication_scheme_managed_identity ? "name: ${var.environments[local.plan_key].agent_pool_name}" : "vmImage: ubuntu-latest" + agent_pool_configuration_apply = local.is_authentication_scheme_managed_identity ? "name: ${var.environments[local.apply_key].agent_pool_name}" : "vmImage: ubuntu-latest" + service_connection_plan_name = var.environments[local.plan_key].service_connection_name + service_connection_apply_name = var.environments[local.apply_key].service_connection_name + environment_name_plan = var.environments[local.plan_key].environment_name + environment_name_apply = var.environments[local.apply_key].environment_name + + cicd_file = { for key, value in var.repository_files : key => + { + content = templatefile(value.path, { + project_name = var.project_name + repository_name_templates = local.repository_name_templates + ci_template_path = var.pipeline_templates.ci.target_path + cd_template_path = var.pipeline_templates.cd.target_path + }) + } if value.flag == "pipeline" + } + cicd_template_files = { for key, value in var.repository_files : key => + { + content = templatefile(value.path, { + agent_pool_configuration_plan = local.agent_pool_configuration_plan + agent_pool_configuration_apply = local.agent_pool_configuration_apply + environment_name_plan = local.environment_name_plan + environment_name_apply = local.environment_name_apply + variable_group_name = var.variable_group_name + project_name = var.project_name + repository_name_templates = local.repository_name_templates + service_connection_name_plan = local.service_connection_plan_name + service_connection_name_apply = local.service_connection_apply_name + }) + } if value.flag == "pipeline_template" + } + module_files = { for key, value in var.repository_files : key => + { + content = replace((file(value.path)), "# backend \"azurerm\" {}", "backend \"azurerm\" {}") + } if value.flag == "module" || value.flag == "additional" + } + repository_files = merge(local.cicd_file, local.module_files, var.use_template_repository ? {} : local.cicd_template_files) +} diff --git a/bootstrap/modules/azure_devops/locals_pipelines.tf b/bootstrap/modules/azure_devops/locals_pipelines.tf new file mode 100644 index 00000000..352ea4d3 --- /dev/null +++ b/bootstrap/modules/azure_devops/locals_pipelines.tf @@ -0,0 +1,69 @@ +locals { + pipelines = { for key, value in var.pipelines : key => { + pipeline_name = value.pipeline_name + file = azuredevops_git_repository_file.alz[value.target_path].file + environments = [for environment_key in value.environment_keys : { + environment_key = environment_key + environment_id = azuredevops_environment.alz[environment_key].id + }] + service_connections = [for service_connection_key in value.service_connection_keys : + { + service_connection_key = service_connection_key + service_connection_id = azuredevops_serviceendpoint_azurerm.alz[service_connection_key].id + }] + agent_pools = local.is_authentication_scheme_managed_identity ? [for agent_pool_key in value.agent_pool_keys : + { + agent_pool_key = agent_pool_key + agent_pool_id = azuredevops_agent_queue.alz[agent_pool_key].id + }] : [] + } + } + + pipeline_environments = flatten([for pipeline_key, pipeline in local.pipelines : + [for environment in pipeline.environments : { + pipeline_key = pipeline_key + environment_key = environment.environment_key + pipeline_id = azuredevops_build_definition.alz[pipeline_key].id + environment_id = environment.environment_id + } + ] + ]) + + pipeline_service_connections = flatten([for pipeline_key, pipeline in local.pipelines : + [for service_connection in pipeline.service_connections : { + pipeline_key = pipeline_key + service_connection_key = service_connection.service_connection_key + pipeline_id = azuredevops_build_definition.alz[pipeline_key].id + service_connection_id = service_connection.service_connection_id + } + ] + ]) + + pipeline_agent_pools = local.is_authentication_scheme_managed_identity ? flatten([for pipeline_key, pipeline in local.pipelines : + [for agent_pool in pipeline.agent_pools : { + pipeline_key = pipeline_key + agent_pool_key = agent_pool.agent_pool_key + pipeline_id = azuredevops_build_definition.alz[pipeline_key].id + agent_pool_id = agent_pool.agent_pool_id + } + ] + ]) : [] + + pipeline_environments_map = { for pipeline_environment in local.pipeline_environments : "${pipeline_environment.pipeline_key}-${pipeline_environment.environment_key}" => { + pipeline_id = pipeline_environment.pipeline_id + environment_id = pipeline_environment.environment_id + } + } + + pipeline_service_connections_map = { for pipeline_service_connection in local.pipeline_service_connections : "${pipeline_service_connection.pipeline_key}-${pipeline_service_connection.service_connection_key}" => { + pipeline_id = pipeline_service_connection.pipeline_id + service_connection_id = pipeline_service_connection.service_connection_id + } + } + + pipeline_agent_pools_map = { for pipeline_agent_pool in local.pipeline_agent_pools : "${pipeline_agent_pool.pipeline_key}-${pipeline_agent_pool.agent_pool_key}" => { + pipeline_id = pipeline_agent_pool.pipeline_id + agent_pool_id = pipeline_agent_pool.agent_pool_id + } + } +} diff --git a/bootstrap/modules/azure_devops/pipeline.tf b/bootstrap/modules/azure_devops/pipeline.tf index 978d42ec..a70d5238 100644 --- a/bootstrap/modules/azure_devops/pipeline.tf +++ b/bootstrap/modules/azure_devops/pipeline.tf @@ -1,20 +1,7 @@ -locals { - pipelines = { - ci = { - name = "Azure Landing Zone Continuous Integration" - file = azuredevops_git_repository_file.alz[var.pipeline_ci_file].file - } - cd = { - name = "Azure Landing Zone Continuous Delivery" - file = azuredevops_git_repository_file.alz[var.pipeline_cd_file].file - } - } -} - resource "azuredevops_build_definition" "alz" { for_each = local.pipelines project_id = local.project_id - name = each.value.name + name = each.value.pipeline_name ci_trigger { use_yaml = true @@ -28,49 +15,26 @@ resource "azuredevops_build_definition" "alz" { } } -resource "azuredevops_pipeline_authorization" "alz_environment_plan" { - for_each = local.pipelines - project_id = local.project_id - resource_id = azuredevops_environment.alz[local.plan_key].id - type = "environment" - pipeline_id = azuredevops_build_definition.alz[each.key].id -} - -resource "azuredevops_pipeline_authorization" "alz_environment_apply" { - for_each = local.pipelines +resource "azuredevops_pipeline_authorization" "alz_environment" { + for_each = local.pipeline_environments_map project_id = local.project_id - resource_id = azuredevops_environment.alz[local.apply_key].id + resource_id = each.value.environment_id type = "environment" - pipeline_id = azuredevops_build_definition.alz[each.key].id + pipeline_id = each.value.pipeline_id } -resource "azuredevops_pipeline_authorization" "alz_service_connection_plan" { +resource "azuredevops_pipeline_authorization" "alz_service_connection" { + for_each = local.pipeline_service_connections_map project_id = local.project_id - resource_id = azuredevops_serviceendpoint_azurerm.alz[local.plan_key].id + resource_id = each.value.service_connection_id type = "endpoint" - pipeline_id = azuredevops_build_definition.alz["ci"].id -} - -resource "azuredevops_pipeline_authorization" "alz_service_connection_apply" { - for_each = var.environments - project_id = local.project_id - resource_id = azuredevops_serviceendpoint_azurerm.alz[each.key].id - type = "endpoint" - pipeline_id = azuredevops_build_definition.alz["cd"].id -} - -resource "azuredevops_pipeline_authorization" "alz_plan" { - for_each = { for key, value in local.agent_pools : key => value if key == local.plan_key } - project_id = local.project_id - resource_id = azuredevops_agent_queue.alz[local.plan_key].id - type = "queue" - pipeline_id = azuredevops_build_definition.alz["ci"].id + pipeline_id = each.value.pipeline_id } -resource "azuredevops_pipeline_authorization" "alz_apply" { - for_each = local.agent_pools +resource "azuredevops_pipeline_authorization" "alz_agent_pool" { + for_each = local.pipeline_agent_pools_map project_id = local.project_id - resource_id = azuredevops_agent_queue.alz[each.key].id + resource_id = each.value.agent_pool_id type = "queue" - pipeline_id = azuredevops_build_definition.alz["cd"].id + pipeline_id = each.value.pipeline_id } diff --git a/bootstrap/modules/azure_devops/repository.tf b/bootstrap/modules/azure_devops/repository_module.tf similarity index 56% rename from bootstrap/modules/azure_devops/repository.tf rename to bootstrap/modules/azure_devops/repository_module.tf index c818f588..76cc780d 100644 --- a/bootstrap/modules/azure_devops/repository.tf +++ b/bootstrap/modules/azure_devops/repository_module.tf @@ -1,7 +1,3 @@ -locals { - default_branch = "refs/heads/main" -} - resource "azuredevops_git_repository" "alz" { depends_on = [azuredevops_environment.alz] project_id = local.project_id @@ -12,35 +8,6 @@ resource "azuredevops_git_repository" "alz" { } } -locals { - agent_pool_configuration_plan = local.is_authentication_scheme_managed_identity ? "name: ${var.environments[local.plan_key].agent_pool_name}" : "vmImage: ubuntu-latest" - agent_pool_configuration_apply = local.is_authentication_scheme_managed_identity ? "name: ${var.environments[local.apply_key].agent_pool_name}" : "vmImage: ubuntu-latest" - service_connection_plan_name = var.environments[local.plan_key].service_connection_name - service_connection_apply_name = var.environments[local.apply_key].service_connection_name - environment_name_plan = var.environments[local.plan_key].environment_name - environment_name_apply = var.environments[local.apply_key].environment_name - - cicd_file = { for key, value in var.repository_files : key => - { - content = templatefile(value.path, { - agent_pool_configuration_plan = local.agent_pool_configuration_plan - agent_pool_configuration_apply = local.agent_pool_configuration_apply - service_connection_name_plan = local.service_connection_plan_name - service_connection_name_apply = local.service_connection_apply_name - environment_name_plan = local.environment_name_plan - environment_name_apply = local.environment_name_apply - variable_group_name = var.variable_group_name - }) - } if value.flag == "cicd" - } - module_files = { for key, value in var.repository_files : key => - { - content = replace((file(value.path)), "# backend \"azurerm\" {}", "backend \"azurerm\" {}") - } if value.flag == "module" || value.flag == "additional" - } - repository_files = merge(local.cicd_file, local.module_files) -} - resource "azuredevops_git_repository_file" "alz" { for_each = local.repository_files repository_id = azuredevops_git_repository.alz.id @@ -55,7 +22,7 @@ resource "azuredevops_branch_policy_min_reviewers" "alz" { depends_on = [azuredevops_git_repository_file.alz] project_id = local.project_id - enabled = true + enabled = length(var.approvers) > 1 blocking = true settings { diff --git a/bootstrap/modules/azure_devops/repository_templates.tf b/bootstrap/modules/azure_devops/repository_templates.tf new file mode 100644 index 00000000..241131fc --- /dev/null +++ b/bootstrap/modules/azure_devops/repository_templates.tf @@ -0,0 +1,64 @@ +resource "azuredevops_git_repository" "alz_templates" { + count = var.use_template_repository ? 1 : 0 + project_id = local.project_id + name = var.repository_name_templates + default_branch = local.default_branch + initialization { + init_type = "Clean" + } +} + +resource "azuredevops_git_repository_file" "alz_templates" { + for_each = var.use_template_repository ? local.cicd_template_files : {} + repository_id = azuredevops_git_repository.alz_templates[0].id + file = each.key + content = each.value.content + branch = local.default_branch + commit_message = "Add ${each.key} [skip ci]" + overwrite_on_create = true +} + +resource "azuredevops_branch_policy_min_reviewers" "alz_templates" { + count = var.use_template_repository ? 1 : 0 + depends_on = [azuredevops_git_repository_file.alz_templates] + project_id = local.project_id + + enabled = length(var.approvers) > 1 + blocking = true + + settings { + reviewer_count = 1 + submitter_can_vote = false + last_pusher_cannot_approve = true + allow_completion_with_rejects_or_waits = false + on_push_reset_approved_votes = true + + scope { + repository_id = azuredevops_git_repository.alz_templates[0].id + repository_ref = azuredevops_git_repository.alz_templates[0].default_branch + match_type = "Exact" + } + } +} + +resource "azuredevops_branch_policy_merge_types" "alz_templates" { + count = var.use_template_repository ? 1 : 0 + depends_on = [azuredevops_git_repository_file.alz_templates] + project_id = local.project_id + + enabled = true + blocking = true + + settings { + allow_squash = true + allow_rebase_and_fast_forward = false + allow_basic_no_fast_forward = false + allow_rebase_with_merge = false + + scope { + repository_id = azuredevops_git_repository.alz_templates[0].id + repository_ref = azuredevops_git_repository.alz_templates[0].default_branch + match_type = "Exact" + } + } +} diff --git a/bootstrap/modules/azure_devops/service_connections.tf b/bootstrap/modules/azure_devops/service_connections.tf index 2780e0a4..d828c111 100644 --- a/bootstrap/modules/azure_devops/service_connections.tf +++ b/bootstrap/modules/azure_devops/service_connections.tf @@ -16,3 +16,42 @@ resource "azuredevops_serviceendpoint_azurerm" "alz" { azurerm_subscription_id = var.azure_subscription_id azurerm_subscription_name = var.azure_subscription_name } + +resource "azuredevops_check_approval" "alz" { + count = length(var.approvers) == 0 ? 0 : 1 + project_id = local.project_id + target_resource_id = azuredevops_serviceendpoint_azurerm.alz[local.apply_key].id + target_resource_type = "endpoint" + + requester_can_approve = length(var.approvers) == 1 + approvers = [ + azuredevops_group.alz_approvers.origin_id + ] + + timeout = 43200 +} + +resource "azuredevops_check_exclusive_lock" "alz" { + for_each = var.environments + project_id = local.project_id + target_resource_id = azuredevops_serviceendpoint_azurerm.alz[each.key].id + target_resource_type = "endpoint" + timeout = 43200 +} + +resource "azuredevops_check_required_template" "alz" { + for_each = var.environments + project_id = local.project_id + target_resource_id = azuredevops_serviceendpoint_azurerm.alz[each.key].id + target_resource_type = "endpoint" + + dynamic "required_template" { + for_each = each.value.service_connection_template_keys + content { + repository_type = "azuregit" + repository_name = "${var.project_name}/${local.repository_name_templates}" + repository_ref = "refs/heads/main" + template_path = var.pipeline_templates[required_template.value].target_path + } + } +} diff --git a/bootstrap/modules/azure_devops/variables.tf b/bootstrap/modules/azure_devops/variables.tf index b319c41b..44cf86ea 100644 --- a/bootstrap/modules/azure_devops/variables.tf +++ b/bootstrap/modules/azure_devops/variables.tf @@ -24,9 +24,10 @@ variable "project_name" { variable "environments" { type = map(object({ - environment_name = string - service_connection_name = string - agent_pool_name = string + environment_name = string + service_connection_name = string + service_connection_template_keys = list(string) + agent_pool_name = string })) } @@ -45,12 +46,16 @@ variable "repository_files" { })) } -variable "pipeline_ci_file" { - type = string -} - -variable "pipeline_cd_file" { - type = string +variable "pipelines" { + description = "The pipelines to create|hidden" + type = map(object({ + pipeline_name = string + file_path = string + target_path = string + environment_keys = list(string) + service_connection_keys = list(string) + agent_pool_keys = list(string) + })) } variable "variable_group_name" { @@ -88,3 +93,18 @@ variable "approvers" { variable "group_name" { type = string } + +variable "use_template_repository" { + type = bool +} + +variable "repository_name_templates" { + type = string +} + +variable "pipeline_templates" { + type = map(object({ + target_path = string + file_path = string + })) +} diff --git a/bootstrap/modules/files/main.tf b/bootstrap/modules/files/main.tf index 148cb777..c95d39a1 100644 --- a/bootstrap/modules/files/main.tf +++ b/bootstrap/modules/files/main.tf @@ -1,12 +1,5 @@ locals { - files = fileset(var.folder_path, "**") - filtered_files = length(var.exclusions) == 0 ? sort(local.files) : sort(flatten([ - for f in local.files : [ - for e in var.exclusions : - strcontains(f, e) ? [] : [f] - ] - ])) - file_map = { for file in local.filtered_files : file => { + file_map = { for file in fileset(var.folder_path, var.include) : file => { path = "${var.folder_path}/${file}" flag = var.flag } diff --git a/bootstrap/modules/files/variables.tf b/bootstrap/modules/files/variables.tf index 2827fe14..e58a1fff 100644 --- a/bootstrap/modules/files/variables.tf +++ b/bootstrap/modules/files/variables.tf @@ -3,10 +3,10 @@ variable "folder_path" { type = string } -variable "exclusions" { - description = "List of files / partial file names to exclude" - type = list(string) - default = [] +variable "include" { + description = "Files globs to match as per the fileset documentation: https://www.terraform.io/docs/language/functions/fileset.html" + type = string + default = "**" } variable "flag" { diff --git a/bootstrap/modules/github/local_files.tf b/bootstrap/modules/github/local_files.tf new file mode 100644 index 00000000..cece34b5 --- /dev/null +++ b/bootstrap/modules/github/local_files.tf @@ -0,0 +1,27 @@ +locals { + cicd_file = { for key, value in var.repository_files : key => + { + content = templatefile(value.path, { + organization_name = var.organization_name + repository_name_templates = local.repository_name_templates + ci_template_path = var.pipeline_templates["ci"].target_path + cd_template_path = var.pipeline_templates["cd"].target_path + }) + } if value.flag == "pipeline" + } + cicd_template_files = { for key, value in var.repository_files : key => + { + content = templatefile(value.path, { + environment_name_plan = var.environments[local.plan_key] + environment_name_apply = var.environments[local.apply_key] + backend_azure_storage_account_container_name = var.backend_azure_storage_account_container_name + }) + } if value.flag == "pipeline_template" + } + module_files = { for key, value in var.repository_files : key => + { + content = replace((file(value.path)), "# backend \"azurerm\" {}", "backend \"azurerm\" {}") + } if value.flag == "module" || value.flag == "additional" + } + repository_files = merge(local.cicd_file, local.module_files, var.use_template_repository ? {} : local.cicd_template_files) +} diff --git a/bootstrap/modules/github/locals.tf b/bootstrap/modules/github/locals.tf index 31a7dcdd..117b3e4c 100644 --- a/bootstrap/modules/github/locals.tf +++ b/bootstrap/modules/github/locals.tf @@ -11,3 +11,23 @@ locals { primary_approver = length(var.approvers) > 0 ? var.approvers[0] : "" default_commit_email = coalesce(local.primary_approver, "demo@microsoft.com") } + +locals { + repository_name_templates = var.use_template_repository ? var.repository_name_templates : var.repository_name + template_claim_structure = "${var.organization_name}/${local.repository_name_templates}/%s@refs/heads/main" + + oidc_subjects_flattened = flatten([for key, value in var.pipeline_templates : [ + for environment_user_assigned_managed_identity_mapping in value.environment_user_assigned_managed_identity_mappings : + { + subject_key = "${key}-${environment_user_assigned_managed_identity_mapping.user_assigned_managed_identity_key}" + user_assigned_managed_identity_key = environment_user_assigned_managed_identity_mapping.user_assigned_managed_identity_key + 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.target_path)}" + } + ] + ]) + + oidc_subjects = { for oidc_subject in local.oidc_subjects_flattened : oidc_subject.subject_key => { + user_assigned_managed_identity_key = oidc_subject.user_assigned_managed_identity_key + subject = oidc_subject.subject + } } +} diff --git a/bootstrap/modules/github/oidc_templates.tf b/bootstrap/modules/github/oidc_templates.tf new file mode 100644 index 00000000..20198642 --- /dev/null +++ b/bootstrap/modules/github/oidc_templates.tf @@ -0,0 +1,5 @@ +resource "github_actions_repository_oidc_subject_claim_customization_template" "alz" { + repository = github_repository.alz.name + use_default = false + include_claim_keys = ["repository", "environment", "job_workflow_ref"] +} diff --git a/bootstrap/modules/github/outputs.tf b/bootstrap/modules/github/outputs.tf index d35cbd28..021cb279 100644 --- a/bootstrap/modules/github/outputs.tf +++ b/bootstrap/modules/github/outputs.tf @@ -3,10 +3,7 @@ output "organization_url" { } output "subjects" { - value = { - (local.plan_key) = "repo:${var.organization_name}/${var.repository_name}:environment:${var.environments[local.plan_key]}" - (local.apply_key) = "repo:${var.organization_name}/${var.repository_name}:environment:${var.environments[local.apply_key]}" - } + value = local.oidc_subjects } output "issuer" { diff --git a/bootstrap/modules/github/repository.tf b/bootstrap/modules/github/repository_module.tf similarity index 67% rename from bootstrap/modules/github/repository.tf rename to bootstrap/modules/github/repository_module.tf index d9cdcd33..15dd4aeb 100644 --- a/bootstrap/modules/github/repository.tf +++ b/bootstrap/modules/github/repository_module.tf @@ -8,23 +8,6 @@ resource "github_repository" "alz" { allow_rebase_merge = false } -locals { - cicd_file = { for key, value in var.repository_files : key => - { - content = templatefile(value.path, { - environment_name_plan = var.environments[local.plan_key] - environment_name_apply = var.environments[local.apply_key] - }) - } if value.flag == "cicd" - } - module_files = { for key, value in var.repository_files : key => - { - content = replace((file(value.path)), "# backend \"azurerm\" {}", "backend \"azurerm\" {}") - } if value.flag == "module" || value.flag == "additional" - } - repository_files = merge(local.cicd_file, local.module_files) -} - resource "github_repository_file" "alz" { for_each = local.repository_files repository = github_repository.alz.name diff --git a/bootstrap/modules/github/repository_templates.tf b/bootstrap/modules/github/repository_templates.tf new file mode 100644 index 00000000..5aada875 --- /dev/null +++ b/bootstrap/modules/github/repository_templates.tf @@ -0,0 +1,37 @@ +resource "github_repository" "alz_templates" { + count = var.use_template_repository ? 1 : 0 + name = var.repository_name_templates + description = var.repository_name_templates + auto_init = true + visibility = var.repository_visibility + allow_update_branch = true + allow_merge_commit = false + allow_rebase_merge = false +} + +resource "github_repository_file" "alz_templates" { + for_each = var.use_template_repository ? local.cicd_template_files : {} + repository = github_repository.alz_templates[0].name + file = each.key + content = each.value.content + commit_author = local.default_commit_email + commit_email = local.default_commit_email + commit_message = "Add ${each.key} [skip ci]" + overwrite_on_create = true +} + +resource "github_branch_protection" "alz_templates" { + count = var.use_template_repository ? 1 : 0 + depends_on = [github_repository_file.alz_templates] + repository_id = github_repository.alz_templates[0].name + pattern = "main" + enforce_admins = true + required_linear_history = true + require_conversation_resolution = true + + required_pull_request_reviews { + dismiss_stale_reviews = true + restrict_dismissals = true + required_approving_review_count = length(var.approvers) > 1 ? 1 : 0 + } +} diff --git a/bootstrap/modules/github/variables.tf b/bootstrap/modules/github/variables.tf index 50515aa5..8e372bb7 100644 --- a/bootstrap/modules/github/variables.tf +++ b/bootstrap/modules/github/variables.tf @@ -52,3 +52,22 @@ variable "approvers" { variable "team_name" { type = string } + +variable "use_template_repository" { + type = bool +} + +variable "repository_name_templates" { + type = string +} + +variable "pipeline_templates" { + type = map(object({ + target_path = string + file_path = string + environment_user_assigned_managed_identity_mappings = list(object({ + environment_key = string + user_assigned_managed_identity_key = string + })) + })) +} diff --git a/bootstrap/modules/resource_names/locals.tf b/bootstrap/modules/resource_names/locals.tf index f496fe24..8f723177 100644 --- a/bootstrap/modules/resource_names/locals.tf +++ b/bootstrap/modules/resource_names/locals.tf @@ -3,6 +3,7 @@ resource "random_string" "alz" { length = 4 special = false upper = false + numeric = false } locals { diff --git a/docs/wiki/Frequently-Asked-Questions.md b/docs/wiki/Frequently-Asked-Questions.md index 36e2eb2c..934b3ab8 100644 --- a/docs/wiki/Frequently-Asked-Questions.md +++ b/docs/wiki/Frequently-Asked-Questions.md @@ -147,13 +147,20 @@ First you'll need to create a folder structure to hold your custom starter modul ```text 📦my-custom-starter-modules #1 ┣ 📂my-ci-cd #2 - ┃ ┣ 📂.azuredevops #3 - ┃ ┃ ┣ 📜my-cd.yaml #4 - ┃ ┃ ┗ 📜my-ci.yaml - ┃ ┗ 📂.github - ┃ ┃ ┗ 📂workflows - ┃ ┃ ┃ ┣ 📜my-cd.yaml - ┃ ┃ ┃ ┗ 📜my-ci.yaml + ┃ ┣ 📂azuredevops #3 + ┃ ┃ ┣ 📜cd.yaml + ┃ ┃ ┣ 📜ci.yaml + ┃ ┃ ┗ 📂templates #4 + ┃ ┃ ┣ 📜apply.yaml + ┃ ┃ ┣ 📜cd.yaml + ┃ ┃ ┣ 📜ci.yaml + ┃ ┃ ┗ 📜plan.yaml + ┃ ┗ 📂github + ┃ ┣ 📜cd.yaml + ┃ ┣ 📜ci.yaml + ┃ ┗ 📂templates + ┃ ┣ 📜cd.yaml + ┃ ┗ 📜ci.yaml ┣ 📂my-starter-module-1 #5 ┃ ┣ 📜main.tf ┃ ┣ 📜outputs.tf @@ -161,37 +168,37 @@ First you'll need to create a folder structure to hold your custom starter modul ┃ ┣ 📜README.md ┃ ┣ 📜terraform.tfvars ┃ ┗ 📜variables.tf #6 - ┣ 📂my-starter-module-2 - ┃ ┣ 📜data.tf - ┃ ┣ 📜main.tf - ┃ ┣ 📜variables.tf - ┃ ┗ 📜versions.tf + ┗ 📂my-starter-module-2 + ┣ 📜data.tf + ┣ 📜main.tf + ┣ 📜variables.tf + ┗ 📜versions.tf ``` Notes on the folder structure: -1. This is the enclosing folder as specified in the `template_folder_path` variable (see below). -1. This is the CI / CD actions / pipelines folder as specified in `ci_cd_module` variable (see below). -1. You only need to supply one of either `.azuredevops` or `.github\workflows` folder if you are only using one VCS system. The GitHub folder name cannot be altered, but Azure DevOps can if desired. -1. If you change the name of these files from `ci.yaml` or `cd.yaml` for Azure DevOps, you need to specify them in the `ci_file_path` and `cd_file_path` variables as specified below. These files are templated, so please use the existing ones as a guide if you plan to update them. -1. This is an example starter module folder. This will also the name of the starter module as supplied to the `starter_module` input. -1. Variables must be stored in a file called `variables.tf`. If you need validation, etc, please follow our examples. These variables are translated into inputs to the PowerShell module. +1. This is the enclosing folder path as specified in the `module_folder_path` variable (see below). +2. This is the CI / CD actions / pipelines folder path as specified in `pipeline_folder_path` variable (see below). This folder can be outside the module folder if desired. +3. You only need to supply one of either `azuredevops` or `github` folder if you are only using one VCS system. The folder and file names can't be altered at present. +4. This is the templates folder used for the cd, cd, plan and apply templates. +5. This is an example starter module folder. This will also the name of the starter module as supplied to the `starter_module` input. +6. Variables must be stored in a file called `variables.tf`. If you need validation, etc, please follow our examples. These variables are translated into inputs to the PowerShell module. -Next, you'll need to override the starter template folder location in the PowerShell module. To do that, create yaml or json file that provides values for the `template_folder_path` and optionally the `ci_cd_module` variables. For example: +Next, you'll need to override the starter template folder location in the PowerShell module. To do that, create yaml or json file that provides values for the `module_folder_path` and the `pipeline_folder_path` variables. For example: ```yaml -template_folder_path: "C:/my-config/my-custom-starter-modules" # This is the folder you created in the last step -ci_cd_module: "my-ci-cd" # This is the name of the CI / CD folder -ci_file_path: ".azuredevops/my-ci.yaml" # This variable is only required if are using Azure DevOps and have updated the CI file path / name. -cd_file_path: ".azuredevops/my-cd.yaml" # This variable is only required if are using Azure DevOps and have updated the CD file path / name. +module_folder_path: "C:/my-config/my-custom-starter-modules" # This is the folder you created in the last step +module_folder_path_relative: false # You must specifiy this as false if you are using a custom starter module folder +pipeline_folder_path: "C:/my-config/my-custom-starter-modules/my_ci_cd" # This is the pipeline folder you created in the last step (NOTE: This does not need to be nested under the module folder, it could be in a separate location) +pipeline_folder_path_relative: false # You must specifiy this as false if you are using a custom pipeline module folder ``` ```json { - "template_folder_path": "~/my-config/my-custom-starter-modules", - "ci_cd_module": "my-ci-cd", - "ci_file_path": ".azuredevops/my-ci.yaml", - "cd_file_path": ".azuredevops/my-cd.yaml" + "module_folder_path": "~/my-config/my-custom-starter-modules", + "module_folder_path_relative": false, + "pipeline_folder_path": "~/my-config/my-custom-starter-modules/my_ci_cd", + "pipeline_folder_path_relative": false } ``` diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index c7050e85..8b072b68 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -27,11 +27,12 @@ We only support federated credentials for GitHub as a best practice. - Resource Group for State - Storage Account and Container for State - Resource Group for Identity - - User Assigned Managed Identity (UAMI) with Federated Credentials + - User Assigned Managed Identities (UAMI) with Federated Credentials for Plan and Apply - Permissions for the UAMI on state storage container, subscriptions and management groups - GitHub - - Repository + - Repository for the Module + - Repository for the Action Templates - Starter Terraform module with tfvars - Branch policy - Action for Continuous Integration @@ -40,6 +41,7 @@ We only support federated credentials for GitHub as a best practice. - Environment for Apply - Action Variables for Backend and Plan / Apply - Team and Members for Apply Approval + - Customised OIDC Token Subject for governed Actions ### Azure DevOps with Workload identity federation (WIF / OIDC) @@ -49,12 +51,13 @@ This is the recommended authentication method for Azure DevOps. - Resource Group for State - Storage Account and Container for State - Resource Group for Identity - - User Assigned Managed Identity (UAMI) with Federated Credentials + - User Assigned Managed Identities (UAMI) with Federated Credentials for Plan and Apply - Permissions for the UAMI on state storage container, subscriptions and management groups - Azure DevOps - Project (can be supplied or created) - - Repository + - Repository for the Module + - Repository for the Pipeline Templates - Starter Terraform module with tfvars - Branch policy - Pipeline for Continuous Integration @@ -62,7 +65,8 @@ This is the recommended authentication method for Azure DevOps. - Environment for Plan - Environment for Apply - Variable Group for Backend - - Service Connection with Workload identity federation for Plan / Apply + - Service Connections with Workload identity federation for Plan and Apply + - Service Connection Approvals, Template Validation and Concurrency Control - Group and Members for Apply Approval ### Azure DevOps with Managed identity and self-hosted agents @@ -73,14 +77,15 @@ We include this option as Workload identity federation (WIF) is still in preview - Resource Group for State - Storage Account and Container for State - Resource Group for Identity - - User Assigned Managed Identity (UAMI) + - User Assigned Managed Identities (UAMI) for Plan and Apply - Permissions for the UAMI on state storage container, subscriptions and management groups - Resource Group for Agents - 2 Container Instances with UAMI hosting Azure DevOps Agents - Azure DevOps - Project (can be supplied or created) - - Repository + - Repository for the Module + - Repository for the Pipeline Templates - Starter Terraform module with tfvars - Branch policy - Pipeline for Continuous Integration @@ -88,7 +93,8 @@ We include this option as Workload identity federation (WIF) is still in preview - Environment for Plan - Environment for Apply - Variable Group for Backend - - Service Connection with Managed identity for Plan / Apply + - Service Connections with Managed identity for Plan and Apply + - Service Connection Approvals, Template Validation and Concurrency Control - Group and Members for Apply Approval - Agent Pool diff --git a/docs/wiki/[User-Guide]-Quick-Start-Phase-2.md b/docs/wiki/[User-Guide]-Quick-Start-Phase-2.md index 08a01b2e..66326a59 100644 --- a/docs/wiki/[User-Guide]-Quick-Start-Phase-2.md +++ b/docs/wiki/[User-Guide]-Quick-Start-Phase-2.md @@ -20,6 +20,7 @@ The inputs differ depending on the VCS you have chosen: 1. `starter_module`: This is the choice of [Starter Modules][wiki_starter_modules], which is the baseline configuration you want for your Azure landing zone. This also determine the second set of input you'll be prompted for here. 1. `version_control_system_access_token`: Enter the Azure DevOps PAT you generated in a previous step. 1. `version_control_system_organization`: Enter the name of your Azure DevOps organization. If you are using a self-hosted Azure DevOps Server, supply the fqdn, e.g. `https://vcs.company.com/my-org`. + 1. `version_control_system_use_separate_repository_for_templates`: Determine whether to create a separate repository to store pipeline templates as an extra layer of security. Set to `false` if you don't wish to secure you pipeline templates by using a separate repository. This will default to `true`. 1. `azure_location`: Enter the Azure region where you would like to deploy the storage account and identity for your continuous delivery pipeline. This field expects the `name` of the region, such as `uksouth`. You can find a full list of names by running `az account list-locations -o table`. 1. `azure_subscription_id`: Enter the id of the subscription in which you would like to deploy the storage account and identity for your continuous delivery pipeline. If left blank, the subscription you are connected to via `az login` will be used. 1. `service_name`: This is used to build up the names of your Azure and Azure DevOps resources, for example `rg--mgmt-uksouth-001`. We recommend using `alz` for this. @@ -46,6 +47,7 @@ The inputs differ depending on the VCS you have chosen: 1. `starter_module`: This is the choice of [Starter Module][wiki_starter_modules], which is the baseline configuration you want for your Azure landing zone. This also determine the second set of input you'll be prompted for here. 1. `version_control_system_access_token`: Enter the GitHub PAT you generated in a previous step. 1. `version_control_system_organization`: Enter the name of your GitHub organization. If you are using a self-hosted GitHub Enterprise Server, supply the fqdn, e.g. `https://vcs.company.com/my-org`. + 1. `version_control_system_use_separate_repository_for_templates`: Determine whether to create a separate repository to store pipeline templates as an extra layer of security. Set to `false` if you don't wish to secure you pipeline templates by using a separate repository. This will default to `true`. 1. `azure_location`: Enter the Azure region where you would like to deploy the storage account and identity for your continuous delivery pipeline. This field expects the `name` of the region, such as `uksouth`. You can find a full list of names by running `az account list-locations -o table`. 1. `azure_subscription_id`: Enter the id of the subscription in which you would like to deploy the storage account and identity for your continuous delivery pipeline. If left blank, the subscription you are connected to via `az login` will be used. 1. `service_name`: This is used to build up the names of your Azure and GitHub resources, for example `rg--mgmt-uksouth-001`. We recommend using `alz` for this. diff --git a/templates/.ci_cd/.azuredevops/cd.yaml b/templates/.ci_cd/.azuredevops/cd.yaml deleted file mode 100644 index 1f6b1123..00000000 --- a/templates/.ci_cd/.azuredevops/cd.yaml +++ /dev/null @@ -1,127 +0,0 @@ ---- -trigger: - branches: - include: - - main - -parameters: - - name: terraform_action - displayName: Terraform Action to perform - type: string - default: 'apply' - values: - - 'apply' - - 'destroy' - -lockBehavior: sequential -stages: - - stage: plan - displayName: Plan - variables: - - group: ${variable_group_name} - jobs: - - deployment: plan - displayName: Plan with Terraform - pool: - ${agent_pool_configuration_plan} - environment: ${environment_name_plan} - strategy: - runOnce: - deploy: - steps: - - checkout: self - displayName: Checkout Terraform Module - - task: TerraformInstaller@0 - displayName: Install Terraform - inputs: - terraformVersion: 'latest' - - task: TerraformTaskV4@4 - displayName: Terraform Init - inputs: - provider: 'azurerm' - command: 'init' - backendServiceArm: '${service_connection_name_plan}' - backendAzureRmResourceGroupName: '$(BACKEND_AZURE_RESOURCE_GROUP_NAME)' - backendAzureRmStorageAccountName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_NAME)' - backendAzureRmContainerName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME)' - backendAzureRmKey: 'terraform.tfstate' - env: - ARM_USE_AZUREAD: true - - task: TerraformTaskV4@4 - displayName: Terraform Plan for $${{ coalesce(parameters.terraform_action, 'Apply') }} - inputs: - provider: 'azurerm' - command: 'plan' - $${{ if eq(coalesce(parameters.terraform_action, 'apply'), 'apply') }}: - commandOptions: '-out=tfplan -input=false' - $${{ if eq(coalesce(parameters.terraform_action, 'apply'), 'destroy') }}: - commandOptions: "-out=tfplan -input=false -destroy" - environmentServiceNameAzureRM: '${service_connection_name_plan}' - env: - ARM_USE_AZUREAD: true - - task: CopyFiles@2 - displayName: Create Module Artifact - inputs: - SourceFolder: '$(Build.SourcesDirectory)' - Contents: | - *.tf - *.tfvars - tfplan - TargetFolder: '$(Build.ArtifactsStagingDirectory)' - CleanTargetFolder: true - OverWrite: true - - task: PublishPipelineArtifact@1 - displayName: Publish Module Artifact - inputs: - targetPath: '$(Build.ArtifactsStagingDirectory)' - artifact: 'module' - publishLocation: 'pipeline' - - pwsh: terraform show tfplan - displayName: Show the Plan for Review - - stage: apply - displayName: Apply - dependsOn: plan - variables: - - group: ${variable_group_name} - jobs: - - deployment: apply - displayName: Apply with Terraform - pool: - ${agent_pool_configuration_apply} - environment: ${environment_name_apply} - strategy: - runOnce: - deploy: - steps: - - download: none - - task: DownloadPipelineArtifact@2 - displayName: Download Module Artifact - inputs: - buildType: 'current' - artifactName: 'module' - targetPath: '$(Build.SourcesDirectory)' - - task: TerraformInstaller@0 - displayName: Install Terraform - inputs: - terraformVersion: 'latest' - - task: TerraformTaskV4@4 - displayName: Terraform Init - inputs: - provider: 'azurerm' - command: 'init' - backendServiceArm: '${service_connection_name_apply}' - backendAzureRmResourceGroupName: '$(BACKEND_AZURE_RESOURCE_GROUP_NAME)' - backendAzureRmStorageAccountName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_NAME)' - backendAzureRmContainerName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME)' - backendAzureRmKey: 'terraform.tfstate' - env: - ARM_USE_AZUREAD: true - - task: TerraformTaskV4@4 - displayName: Terraform $${{ coalesce(parameters.terraform_action, 'Apply') }} - inputs: - provider: 'azurerm' - command: 'apply' - commandOptions: '-auto-approve tfplan' - environmentServiceNameAzureRM: '${service_connection_name_apply}' - env: - ARM_USE_AZUREAD: true diff --git a/templates/.ci_cd/.azuredevops/ci.yaml b/templates/.ci_cd/.azuredevops/ci.yaml deleted file mode 100644 index c8b3ff85..00000000 --- a/templates/.ci_cd/.azuredevops/ci.yaml +++ /dev/null @@ -1,62 +0,0 @@ ---- -trigger: - - none - -lockBehavior: sequential -stages: - - stage: validate - displayName: Validation Terraform - variables: - - group: ${variable_group_name} - jobs: - - job: validate - displayName: Validate Terraform - pool: - vmImage: ubuntu-latest - steps: - - task: TerraformInstaller@0 - displayName: Install Terraform - inputs: - terraformVersion: 'latest' - - pwsh: terraform fmt -check - displayName: Terraform Format Check - - pwsh: terraform init -backend=false - displayName: Terraform Init - - pwsh: terraform validate - displayName: Terraform Validate - - deployment: plan - dependsOn: validate - displayName: Validate Terraform Plan - pool: - ${agent_pool_configuration_plan} - environment: ${environment_name_plan} - strategy: - runOnce: - deploy: - steps: - - checkout: self - displayName: Checkout Terraform Module - - task: TerraformInstaller@0 - displayName: Install Terraform - inputs: - terraformVersion: 'latest' - - task: TerraformTaskV4@4 - displayName: Terraform Init - inputs: - provider: 'azurerm' - command: 'init' - backendServiceArm: '${service_connection_name_plan}' - backendAzureRmResourceGroupName: '$(BACKEND_AZURE_RESOURCE_GROUP_NAME)' - backendAzureRmStorageAccountName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_NAME)' - backendAzureRmContainerName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME)' - backendAzureRmKey: 'terraform.tfstate' - env: - ARM_USE_AZUREAD: true - - task: TerraformTaskV4@4 - displayName: Terraform Plan - inputs: - provider: 'azurerm' - command: 'plan' - environmentServiceNameAzureRM: '${service_connection_name_plan}' - env: - ARM_USE_AZUREAD: true diff --git a/templates/ci_cd/azuredevops/cd.yaml b/templates/ci_cd/azuredevops/cd.yaml new file mode 100644 index 00000000..61de64b4 --- /dev/null +++ b/templates/ci_cd/azuredevops/cd.yaml @@ -0,0 +1,27 @@ +--- +trigger: + branches: + include: + - main + +resources: + repositories: + - repository: templates + type: git + name: ${project_name}/${repository_name_templates} + +parameters: + - name: terraform_action + displayName: Terraform Action to perform + type: string + default: 'apply' + values: + - 'apply' + - 'destroy' + +lockBehavior: sequential + +extends: + template: ${cd_template_path}@templates + parameters: + terraform_action: $${{ parameters.terraform_action }} diff --git a/templates/ci_cd/azuredevops/ci.yaml b/templates/ci_cd/azuredevops/ci.yaml new file mode 100644 index 00000000..e6be7d9c --- /dev/null +++ b/templates/ci_cd/azuredevops/ci.yaml @@ -0,0 +1,14 @@ +--- +trigger: + - none + +resources: + repositories: + - repository: templates + type: git + name: ${project_name}/${repository_name_templates} + +lockBehavior: sequential + +extends: + template: ${ci_template_path}@templates diff --git a/templates/ci_cd/azuredevops/templates/apply.yaml b/templates/ci_cd/azuredevops/templates/apply.yaml new file mode 100644 index 00000000..ff6706c0 --- /dev/null +++ b/templates/ci_cd/azuredevops/templates/apply.yaml @@ -0,0 +1,31 @@ +--- +parameters: + - name: terraform_action + default: 'apply' + +steps: + - task: TerraformInstaller@0 + displayName: Install Terraform + inputs: + terraformVersion: 'latest' + - task: TerraformTaskV4@4 + displayName: Terraform Init + inputs: + provider: 'azurerm' + command: 'init' + backendServiceArm: '${service_connection_name_apply}' + backendAzureRmResourceGroupName: '$(BACKEND_AZURE_RESOURCE_GROUP_NAME)' + backendAzureRmStorageAccountName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_NAME)' + backendAzureRmContainerName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME)' + backendAzureRmKey: 'terraform.tfstate' + env: + ARM_USE_AZUREAD: true + - task: TerraformTaskV4@4 + displayName: Terraform $${{ coalesce(parameters.terraform_action, 'Apply') }} + inputs: + provider: 'azurerm' + command: 'apply' + commandOptions: '-auto-approve tfplan' + environmentServiceNameAzureRM: '${service_connection_name_apply}' + env: + ARM_USE_AZUREAD: true diff --git a/templates/ci_cd/azuredevops/templates/cd.yaml b/templates/ci_cd/azuredevops/templates/cd.yaml new file mode 100644 index 00000000..dca81793 --- /dev/null +++ b/templates/ci_cd/azuredevops/templates/cd.yaml @@ -0,0 +1,69 @@ +--- +parameters: + - name: terraform_action + default: 'apply' + +stages: + - stage: plan + displayName: Plan + variables: + - group: ${variable_group_name} + jobs: + - deployment: plan + displayName: Plan with Terraform + pool: + ${agent_pool_configuration_plan} + environment: ${environment_name_plan} + strategy: + runOnce: + deploy: + steps: + - checkout: self + displayName: Checkout Terraform Module + - template: plan.yaml + parameters: + terraform_action: $${{ parameters.terraform_action }} + - task: CopyFiles@2 + displayName: Create Module Artifact + inputs: + SourceFolder: '$(Build.SourcesDirectory)' + Contents: | + *.tf + *.tfvars + tfplan + TargetFolder: '$(Build.ArtifactsStagingDirectory)' + CleanTargetFolder: true + OverWrite: true + - task: PublishPipelineArtifact@1 + displayName: Publish Module Artifact + inputs: + targetPath: '$(Build.ArtifactsStagingDirectory)' + artifact: 'module' + publishLocation: 'pipeline' + - pwsh: terraform show tfplan + displayName: Show the Plan for Review + - stage: apply + displayName: Apply + dependsOn: plan + variables: + - group: ${variable_group_name} + jobs: + - deployment: apply + displayName: Apply with Terraform + pool: + ${agent_pool_configuration_apply} + environment: ${environment_name_apply} + strategy: + runOnce: + deploy: + steps: + - download: none + - task: DownloadPipelineArtifact@2 + displayName: Download Module Artifact + inputs: + buildType: 'current' + artifactName: 'module' + targetPath: '$(Build.SourcesDirectory)' + - template: apply.yaml + parameters: + terraform_action: $${{ parameters.terraform_action }} diff --git a/templates/ci_cd/azuredevops/templates/ci.yaml b/templates/ci_cd/azuredevops/templates/ci.yaml new file mode 100644 index 00000000..10ed21cb --- /dev/null +++ b/templates/ci_cd/azuredevops/templates/ci.yaml @@ -0,0 +1,35 @@ +--- +stages: + - stage: validate + displayName: Validation Terraform + variables: + - group: ${variable_group_name} + jobs: + - job: validate + displayName: Validate Terraform + pool: + vmImage: ubuntu-latest + steps: + - task: TerraformInstaller@0 + displayName: Install Terraform + inputs: + terraformVersion: 'latest' + - pwsh: terraform fmt -check + displayName: Terraform Format Check + - pwsh: terraform init -backend=false + displayName: Terraform Init + - pwsh: terraform validate + displayName: Terraform Validate + - deployment: plan + dependsOn: validate + displayName: Validate Terraform Plan + pool: + ${agent_pool_configuration_plan} + environment: ${environment_name_plan} + strategy: + runOnce: + deploy: + steps: + - checkout: self + displayName: Checkout Terraform Module + - template: plan.yaml diff --git a/templates/ci_cd/azuredevops/templates/plan.yaml b/templates/ci_cd/azuredevops/templates/plan.yaml new file mode 100644 index 00000000..03a6072c --- /dev/null +++ b/templates/ci_cd/azuredevops/templates/plan.yaml @@ -0,0 +1,34 @@ +--- +parameters: + - name: terraform_action + default: 'apply' + +steps: + - task: TerraformInstaller@0 + displayName: Install Terraform + inputs: + terraformVersion: 'latest' + - task: TerraformTaskV4@4 + displayName: Terraform Init + inputs: + provider: 'azurerm' + command: 'init' + backendServiceArm: '${service_connection_name_plan}' + backendAzureRmResourceGroupName: '$(BACKEND_AZURE_RESOURCE_GROUP_NAME)' + backendAzureRmStorageAccountName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_NAME)' + backendAzureRmContainerName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME)' + backendAzureRmKey: 'terraform.tfstate' + env: + ARM_USE_AZUREAD: true + - task: TerraformTaskV4@4 + displayName: Terraform Plan for $${{ coalesce(parameters.terraform_action, 'Apply') }} + inputs: + provider: 'azurerm' + command: 'plan' + $${{ if eq(coalesce(parameters.terraform_action, 'apply'), 'apply') }}: + commandOptions: '-out=tfplan -input=false' + $${{ if eq(coalesce(parameters.terraform_action, 'apply'), 'destroy') }}: + commandOptions: "-out=tfplan -input=false -destroy" + environmentServiceNameAzureRM: '${service_connection_name_plan}' + env: + ARM_USE_AZUREAD: true diff --git a/templates/ci_cd/github/cd.yaml b/templates/ci_cd/github/cd.yaml new file mode 100644 index 00000000..89e1df64 --- /dev/null +++ b/templates/ci_cd/github/cd.yaml @@ -0,0 +1,23 @@ +--- +name: 02 Azure Landing Zone Continuous Delivery +on: + push: + branches: + - main + workflow_dispatch: + inputs: + terraform_action: + description: 'Terraform Action to perform' + required: true + default: 'apply' + type: choice + options: + - 'apply' + - 'destroy' + +jobs: + plan_and_apply: + uses: ${organization_name}/${repository_name_templates}/${cd_template_path}@main + name: 'CD' + with: + terraform_action: $${{ github.event.inputs.terraform_action }} diff --git a/templates/ci_cd/github/ci.yaml b/templates/ci_cd/github/ci.yaml new file mode 100644 index 00000000..8d744aec --- /dev/null +++ b/templates/ci_cd/github/ci.yaml @@ -0,0 +1,12 @@ +--- +name: 01 Azure Landing Zone Continuous Integration +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + validate_and_plan: + uses: ${organization_name}/${repository_name_templates}/${ci_template_path}@main + name: 'CI' diff --git a/templates/.ci_cd/.github/workflows/cd.yaml b/templates/ci_cd/github/templates/cd.yaml similarity index 89% rename from templates/.ci_cd/.github/workflows/cd.yaml rename to templates/ci_cd/github/templates/cd.yaml index f98c693f..9585ab62 100644 --- a/templates/.ci_cd/.github/workflows/cd.yaml +++ b/templates/ci_cd/github/templates/cd.yaml @@ -1,30 +1,22 @@ --- -name: Azure Landing Zone Continuous Delivery +name: Continuous Delivery on: - push: - branches: - - main - workflow_dispatch: + workflow_call: inputs: terraform_action: description: 'Terraform Action to perform' - required: true default: 'apply' - type: choice - options: - - 'apply' - - 'destroy' - -permissions: - id-token: write - contents: read + type: string jobs: plan: name: Plan with Terraform - concurrency: ${environment_name_plan} - environment: ${environment_name_plan} runs-on: ubuntu-latest + concurrency: ${backend_azure_storage_account_container_name} + environment: ${environment_name_plan} + permissions: + id-token: write + contents: read env: ARM_CLIENT_ID: "$${{ vars.AZURE_CLIENT_ID }}" ARM_SUBSCRIPTION_ID: "$${{ vars.AZURE_SUBSCRIPTION_ID }}" @@ -72,9 +64,12 @@ jobs: apply: needs: plan name: Apply with Terraform - concurrency: ${environment_name_apply} - environment: ${environment_name_apply} runs-on: ubuntu-latest + concurrency: ${backend_azure_storage_account_container_name} + environment: ${environment_name_apply} + permissions: + id-token: write + contents: read env: ARM_CLIENT_ID: "$${{ vars.AZURE_CLIENT_ID }}" ARM_SUBSCRIPTION_ID: "$${{ vars.AZURE_SUBSCRIPTION_ID }}" diff --git a/templates/.ci_cd/.github/workflows/ci.yaml b/templates/ci_cd/github/templates/ci.yaml similarity index 82% rename from templates/.ci_cd/.github/workflows/ci.yaml rename to templates/ci_cd/github/templates/ci.yaml index aeac541e..2a4d00f6 100644 --- a/templates/.ci_cd/.github/workflows/ci.yaml +++ b/templates/ci_cd/github/templates/ci.yaml @@ -1,21 +1,12 @@ --- -name: Azure Landing Zone Continuous Integration +name: Continuous Integration on: - pull_request: - branches: - - main - workflow_dispatch: - -permissions: - id-token: write - contents: read - pull-requests: write + workflow_call: jobs: validate: name: Validate Terraform runs-on: ubuntu-latest - steps: - name: Checkout Code uses: actions/checkout@v4 @@ -36,8 +27,16 @@ jobs: name: Validate Terraform Plan needs: validate runs-on: ubuntu-latest - concurrency: ${environment_name_plan} + concurrency: ${backend_azure_storage_account_container_name} environment: ${environment_name_plan} + permissions: + # NOTE: When modifying the token subject claims and adding `environment`. + # If the `id-token` permission is granted at the workflow level + # and the workflow has at least one job that does not specify an environment + # then the action will fail with an internal error. + id-token: write + contents: read + pull-requests: write env: ARM_CLIENT_ID: "$${{ vars.AZURE_CLIENT_ID }}" ARM_SUBSCRIPTION_ID: "$${{ vars.AZURE_SUBSCRIPTION_ID }}" diff --git a/templates/complete/config.yaml b/templates/complete/config.yaml index e6ad107a..9b76bbfb 100644 --- a/templates/complete/config.yaml +++ b/templates/complete/config.yaml @@ -1,4 +1,5 @@ -archetypes: # `caf-enterprise-scale` module, add inputs as listed on the module registry where necessary. +--- +archetypes: # `caf-enterprise-scale` module, add inputs as listed on the module registry where necessary. root_name: es root_id: Enterprise-Scale deploy_corp_landing_zones: true @@ -24,7 +25,7 @@ archetypes: # `caf-enterprise-scale` module, add inputs as listed on the module management: name: aa-management connectivity: - hubnetworking: # `hubnetworking` module, add inputs as listed on the module registry where necessary. + hubnetworking: # `hubnetworking` module, add inputs as listed on the module registry where necessary. hub_virtual_networks: primary: name: vnet-hub @@ -37,7 +38,7 @@ connectivity: sku_name: AZFW_VNet sku_tier: Standard subnet_address_prefix: 10.0.1.0/24 - virtual_network_gateway: # `vnet-gateway` module, add inputs as listed on the module registry where necessary. + virtual_network_gateway: # `vnet-gateway` module, add inputs as listed on the module registry where necessary. name: vgw-hub sku: VpnGw1 type: Vpn diff --git a/tests/scripts/get-release.ps1 b/tests/scripts/get-release.ps1 index 4a920c9d..9ede7b69 100644 --- a/tests/scripts/get-release.ps1 +++ b/tests/scripts/get-release.ps1 @@ -5,13 +5,12 @@ param ( ) $success = $false -$terraformModuleUrl = "https://github.com/Azure/alz-terraform-accelerator" $releaseTag = "" do { $retryCount++ try { Write-Host "Getting the latest release version" - $releaseTag = Get-ALZGithubRelease -directoryForReleases "." -githubRepoUrl $terraformModuleUrl -release "latest" -queryOnly -ErrorAction Stop + $releaseTag = Get-ALZGithubRelease -o "." -i "terraform" -v "latest" -queryOnly -ErrorAction Stop $success = $true } catch { Write-Host "Failed to get the release version. Retrying after $retryDelay ms..."