diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index dbb4c7c..05be96c 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -2,79 +2,81 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azuread" { - version = "2.14.0" - constraints = ">= 1.4.0, >= 1.5.1" + version = "2.22.0" + constraints = ">= 2.22.0" hashes = [ - "h1:llPDf1aQZeZo9a0YxqJjJYBVByfTmV98GzGS4qH4BUw=", - "zh:0e5fad817e949070fc367808346a8fe22545ff2eaff58ae799f5620e0f3b5d74", - "zh:6480f651297c3db7acaa8d66aa496768543de8d37806d5b3139a5971b65cd57c", - "zh:70f9beafd2f87d9779be087938ba574d8746a0134854d194cf61f48f7ed1409f", - "zh:7be844cc5bc148af429a45a38fbf0875fb17833e43996a2df2ad400a6e699c3d", - "zh:91ad6dd5482a3947512ee17566df255903edeb081b4214cb32292b0b62d1e25a", - "zh:979aabdc49b779f382c09894914bc583a58a81dbac8d079e398088ef95e8d5eb", - "zh:a7abd1cb9b111e40ad7756cb3b49fffef9fbcd750f2a06816d0f6704f2cffe48", - "zh:b8b56f2cab1e853f2da754580a6d0986c69cc37c11a035cedcf1732053911cf6", - "zh:c4685b19e99bb39c6ba6d19796c37566ced97c61dd14f5c33e70e7a1276b0319", - "zh:f4e5b190f228e4b6f3408cf7f04d083bf24b0ddda8992c804f7fc34abf6a60fb", - "zh:fb6f5bc43a39d916f1b050adda14e1b2cbb940776e8fa91d1d24acf28201ded9", + "h1:8AfMLW7SHmCPDODLKpx4QY0kNV4IZBrwDvfS/yAzpTI=", + "zh:062d84c514cd5015af60693ca4f3aece80d358fd7172951546eaba8093065c5b", + "zh:13749654ccd901408c74de2e1d7de43157044c4e739edcc0a66012a6cc6bba7a", + "zh:138c107f6aa554924a241806bca69248af1b7ce79ec93c6eef369886f33eef0a", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:33c656e07492808da0584717a3cd52377dff15ae0f1f5f411321b8de08a7693e", + "zh:4e08570e51742e717a914db5dd15c0a73cd1686e0c1f1a07123d3aa70cc00718", + "zh:4fef3aca24238cead0798d29196c9e2270622091897dba040c21500c2ddb4095", + "zh:614c60e3dfdd17b7d93b9355e057c825bb36e61f5bc25ccbc6550ff7bd726b65", + "zh:65d8789b8b088322d4e27ea6cd9935749980fe0a1b94e8e56f0cca35c34c394e", + "zh:823abd9bbd9f42bc4c5769be033bf734bb81bb20152b7e1c009a6234b849e5b6", + "zh:9c7ece6b3c65253bfef6ee29acc0cac033ec061bd6755c5496a7e5c17997c918", + "zh:fc0ff3e3104ee6e89c2fa3bf6c83ba698062e64165b60acfe7ad00f2161d1250", ] } provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.91.0" - constraints = ">= 2.50.0, >= 2.63.0" + version = "3.4.0" + constraints = ">= 3.4.0" hashes = [ - "h1:Lpey6sJtmgK33lBZQjDeWO5wh4uh/u6oYx9n6yQcOD4=", - "zh:17102231bc42ac91260489377fb0344408185f9233f126b825c0d0bdc873c8ec", - "zh:2454e0683fd8b230c7f30da2afc26bb0e9d699b85409d175a25cd094e4bf7089", - "zh:276fdae42310057bc7847ff4af6bb441408153af2ad72f8931145da21072ba8a", - "zh:29fd177efd83807acaadc788cbf151e0ed19275b00c7600e1b72316f00e0f1ea", - "zh:5d5ec15bbd38fa4d50074ff530e8851b06eeb08048666cde5096d44eeb495e9c", - "zh:7974ae42bdb7f9104c1477760d7227243a34087526b4d3eea138f3110b10fd58", - "zh:a9fd00320e15c53061556e0dd5818d7e0ca4af0713554dced1ede819350edd9a", - "zh:d8a1a3294faabfe0722ed5e553f054a92b2dc03b7f479ca58d67d36621289ce4", - "zh:ec4e798182bde6a9d89869c458d36b02d3acdd7ce118c91e8af2b86f082bf5e0", - "zh:fb1b3f126f823cb4b6e9018136562e9c28f65732ef0d0f11c18d04117c7ae7a3", - "zh:ff0ead2fe3c4c5d597fdc3f2183407ab971f2f435a887ff7af7dac9ae3fa6e86", + "h1:xhN9eEt1oP9me06v9VsrqpbImlZV16NkHpHcKgpZ66k=", + "zh:4e9913fc3378436d19150c334e5906eafb83a4af3a270423cb7cdda94b27371f", + "zh:5b3d0cec2a600dc1f6633baa8fc36368c5c330fd7654861edcfa76f760a8f6a9", + "zh:5e0e1f899027bc182f31d996c9611e5ba27a034c848d7b0519b39e559fc4f38d", + "zh:66e3a1383ed6a0370989f6fd6abcfa63ccf6918ae535108595af57b9c20a9257", + "zh:688493baf6a116a399b737d74c11080051aca1ab087e5cddd14cc683b7e45c76", + "zh:9e471d85d52343e3ba778f3a94626d820fbec97bb589a3ac7a6a0939b9387770", + "zh:be1e85635daca1768f26962a4cbbadbf7fd13d9da8f9f188e938beca542c2ad5", + "zh:c00e14b6aa566eb9995cb0e1611a18fb8650d9f35c7636a7643a1b6e22660226", + "zh:c40711e5021838fd879da4c9e6b8f7e72104ada2adf0f3ba22e1cc32c3c54086", + "zh:cc62f8541de8d79577e57664e4f03c1fca893d455e5fb238d20668389c0f09ee", + "zh:cd9cbb5c6e5ceb5fcc7c4d0cab516ff209667d1b539b8c7436bd5e452c6aba8f", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" + version = "3.1.3" constraints = "~> 3.1.0" hashes = [ - "h1:9cCiLO/Cqr6IUvMDSApCkQItooiYNatZpEXmcu0nnng=", - "h1:rKYu5ZUbXwrLG1w81k7H3nce/Ys6yAxXhWcbtk36HjY=", - "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", - "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", - "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", - "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", - "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", - "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", - "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", - "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", - "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", - "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", - "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", + "h1:LPSVX+oXKGaZmxgtaPf2USxoEsWK/pnhmm/5FKw+PtU=", + "zh:26e07aa32e403303fc212a4367b4d67188ac965c37a9812e07acee1470687a73", + "zh:27386f48e9c9d849fbb5a8828d461fde35e71f6b6c9fc235bc4ae8403eb9c92d", + "zh:5f4edda4c94240297bbd9b83618fd362348cadf6bf24ea65ea0e1844d7ccedc0", + "zh:646313a907126cd5e69f6a9fafe816e9154fccdc04541e06fed02bb3a8fa2d2e", + "zh:7349692932a5d462f8dee1500ab60401594dddb94e9aa6bf6c4c0bd53e91bbb8", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:9034daba8d9b32b35930d168f363af04cecb153d5849a7e4a5966c97c5dc956e", + "zh:bb81dfca59ef5f949ef39f19ea4f4de25479907abc28cdaa36d12ecd7c0a9699", + "zh:bcf7806b99b4c248439ae02c8e21f77aff9fadbc019ce619b929eef09d1221bb", + "zh:d708e14d169e61f326535dd08eecd3811cd4942555a6f8efabc37dbff9c6fc61", + "zh:dc294e19a46e1cefb9e557a7b789c8dd8f319beca99b8c265181bc633dc434cc", + "zh:f9d758ee53c55dc016dd736427b6b0c3c8eb4d0dbbc785b6a3579b0ffedd9e42", ] } provider "registry.terraform.io/microsoft/azuredevops" { - version = "0.1.8" - constraints = ">= 0.1.0, >= 0.1.8" + version = "0.2.1" + constraints = ">= 0.2.0" hashes = [ - "h1:Rz3VcD+7BjK0krTlHzQdDheu2cNG8OUs5HuMsJJh3v8=", - "zh:15086305cb0312624f8391945f0bcc0e77bc25b770a93e8ddc89dd9d94e4dc5f", - "zh:231c80b5cbfc1c73e9a2aefd7c5e21b5e4e58398375cf9e8052fd11e1e656f7b", - "zh:25742514a15b4212839c1276a66c380054dd990ee0452beb3ae8062b23427913", - "zh:42c3c6ef2ba96a7121869ee13f3e351f6b300f31b415a1aaac1bf6ef018c5461", - "zh:451390e74b77b1049d0412eb6b2df7a23979ebae0c0fb1f96ef64f76689f8d69", - "zh:58c6b6ec9b51950c3f7a20b9a5ec6d862da614a34eab3a7e6a80601209da6fdd", - "zh:6715cfdafc1f4704b05a7aa33daac7be320d6a4e9c5fe0400b194e6cf90639a8", - "zh:96a5d90681048bffe593678f3a1ba2a8a542e72d566c37238408fd27a6619c47", - "zh:a18b54e3da609d00d68e5c837bd805834671ce0e3709b9050b3f8a2641bc7259", - "zh:bdf19514cfd1dc8c8d42fdddc432f58ed8b3589da77aa355d3bdf94f7a18d8e1", - "zh:cb0e717394503ccd4dc367a96bd8afdb7bb73dcd350fc708e2ddcc4d5a788508", - "zh:eaf0dc2f51eb8fbe6e220bac9616af52e9f37ff731d23636d03ee314d6d064aa", + "h1:ivb/yTf7VTsMzIBCg+/NH7qYzq48KPgbtv0E7ZLf+2g=", + "zh:230d28f656a66edeed9028ddc6420833dbb81d3ed6327b160cb6590a3301e52e", + "zh:3dae0d821a9e52a210288ba43994354a70a8a8d9ef273d8935979ce562eaa5e0", + "zh:53784786ccf61790f10d2d3bc4ccd1b91f5cb38302ef6a63c92be7f3509abf70", + "zh:763d1c2e7d0c443e6bad18cf94a035658e841c6e536c46d71b20b0dfa9ad8040", + "zh:81c2f1ad1d9392870111dcdf0274e65dbff202dee5abc0985066a266cb0c9365", + "zh:9f2168271c2407c513205d9e2001c44339cfeb93376245236cb80e0ab62b9b67", + "zh:a38a90c94ac3c01f9f49da5fa7c0a61ccfdc3a976347cf8ed2984b3d29aa2b77", + "zh:cb09b26758f5fa6367b6f9ff1f3c33cc2e9d5f61d3b84550d75405ba1617acfa", + "zh:d8076306289d6e1cf8a34598e49015b9e95a362409c9276dd640c22d1c8b39b1", + "zh:dfa9fb0ca886db7d1addabb1c25128ecf5f0ce65749c0ac32b2e36ee965f5dee", + "zh:e020300eac7b560936687caafebfd616b937fdb4888c2246e4d9749bc3cfc968", + "zh:f12de8c46a0bf557d0a11e26b4253d3bc73e63a19d0caa24e1bafd1c8bb4e09c", ] } diff --git a/_override.tf.sample b/_override.tf.sample index 356fdae..43a8bdb 100644 --- a/_override.tf.sample +++ b/_override.tf.sample @@ -1,5 +1,5 @@ -# To deploy this project from a local machine, first remove -# the `.sample` extension from this file before running the +# To deploy this project from a local machine (without a Terraform backend), +# first remove the `.sample` extension from this file before running the # `terraform init` command. # # For details see: diff --git a/backends/backend.hcl.sample b/backends/backend.hcl.sample index e38e57a..5c5b97b 100644 --- a/backends/backend.hcl.sample +++ b/backends/backend.hcl.sample @@ -1,6 +1,6 @@ storage_account_name="STORAGE_ACCOUNT_NAME" container_name="STORAGE_CONTAINER_NAME" key="FILENAME.tfstate" -# To authenticate to the Storage account, pick and uncomment one of the options below: -# sas_token="?sv=2019-12-12…" # or account key -# access_key="…" # or SAS token \ No newline at end of file +# To authenticate to the Storage account, pick and uncomment *one* of the options below: +# sas_token="?sv=2019-12-12…" # use SAS token +# access_key="…" # use Storage Account Access Key \ No newline at end of file diff --git a/environments/README.md b/environments/README.md new file mode 100644 index 0000000..7eab897 --- /dev/null +++ b/environments/README.md @@ -0,0 +1,14 @@ +# Environments Configuration (Optional) + +Define specific variables per environment. Currently used for Azure Resource tags, e.g. `env=dev` vs `env=prod`. + +These custom tags are merged into defaults defined in [`/variables.tf`](./../variables.tf) + +### Usage example + +These values need to be explicitly specified via `-var-file` flag. + +``` +terraform plan -var-file=environments/dev.tfvars +terraform apply -var-file=environments/dev.tfvars +``` diff --git a/environments/dev.tfvars b/environments/dev.tfvars new file mode 100644 index 0000000..49e1816 --- /dev/null +++ b/environments/dev.tfvars @@ -0,0 +1,6 @@ +custom_tags = { + demo-version = "v0.5.0" + env = "dev" + devops-org = "julie-msft" + github = "azure/devops-governance" +} diff --git a/environments/prod.tfvars b/environments/prod.tfvars new file mode 100644 index 0000000..b18b591 --- /dev/null +++ b/environments/prod.tfvars @@ -0,0 +1,6 @@ +custom_tags = { + demo-version = "v0.5.0" + env = "production" + devops-org = "julie-msft" + github = "azure/devops-governance" +} diff --git a/main.tf b/main.tf index 2e16b35..c9e4dff 100644 --- a/main.tf +++ b/main.tf @@ -1,6 +1,6 @@ -# ------ -# Config -# ------ +# ======== +# Config +# ======== data "azurerm_client_config" "current" {} @@ -16,11 +16,12 @@ locals { suffix = random_string.suffix.result application_owners_ids = length(var.application_owners_ids) == 0 ? [data.azurerm_client_config.current.object_id] : var.application_owners_ids superadmins_aad_object_id = var.superadmins_aad_object_id == "" ? data.azurerm_client_config.current.object_id : var.superadmins_aad_object_id # Default to current ARM client + tags = merge(var.default_tags, var.custom_tags) } -# --------------- -# Azure AD Groups -# --------------- +# ================= +# Azure AD Groups +# ================= resource "azuread_group" "groups" { for_each = var.groups @@ -29,22 +30,24 @@ resource "azuread_group" "groups" { security_enabled = true } -# ------------------ -# Service Principals -# ------------------ +# ==================== +# Service Principals +# ==================== # TODO: document use for CI only. Apps should use diff. SP per PILP module "service_principals" { - for_each = var.environments - source = "./modules/service-principal" - name = "${each.value.team}-${each.value.env}-${local.suffix}-ci-sp" - owners = local.application_owners_ids + for_each = var.environments + source = "./modules/service-principal" + name = "${each.value.team}-${each.value.env}-${local.suffix}-ci-sp" + owners_list = local.application_owners_ids } -# ------------------------------ -# Resource Groups ("Workspaces") -# ------------------------------ +# ============================== +# ARM Resources ("Workspaces") +# ============================== + +# Resource Group, Storage Account, and Key Vault module "arm_environments" { for_each = var.environments @@ -54,19 +57,25 @@ module "arm_environments" { admins_group_id = azuread_group.groups["${each.value.team}_admins"].id superadmins_group_id = local.superadmins_aad_object_id service_principal_id = module.service_principals["${each.value.team}_${each.value.env}"].principal_id + tags = local.tags + depends_on = [ + azuread_group.groups, + module.service_principals + ] } -# ------------ -# Azure DevOps -# ------------ +# ============== +# Azure DevOps +# ============== # The following section Bootstraps: # - Projects: Team silos and shared projects # - Security Group Assignments: like Role Assignments in ARM # - Service Connections: service principal credentials created in code above -# Projects -# -------- +# ============== +# ADO Projects +# ============== # Team Projects @@ -120,8 +129,9 @@ resource "azuredevops_project" "collaboration" { } } -# Security Group Assignments -# -------------------------- +# ================================ +# ADO Security Group Assignments +# ================================ # Teams Silo Projects - Security Group Assignments @@ -191,8 +201,9 @@ module "ado_collaboration_permissions_veggies" { ] } -# Service Connections -# ------------------- +# ========================= +# ADO Service Connections +# ========================= module "service_connections" { for_each = module.arm_environments @@ -203,6 +214,7 @@ module "service_connections" { depends_on = [ azuread_group.groups, + azuredevops_project.team_projects, module.arm_environments, module.service_principals ] diff --git a/modules/azure-devops-permissions/provider.tf b/modules/azure-devops-permissions/provider.tf index 6ae5e62..0453d73 100644 --- a/modules/azure-devops-permissions/provider.tf +++ b/modules/azure-devops-permissions/provider.tf @@ -2,7 +2,7 @@ terraform { required_providers { azuredevops = { source = "microsoft/azuredevops" - version = ">=0.1.0" + version = ">=0.2.0" } } } diff --git a/modules/azure-devops-service-connection/main.tf b/modules/azure-devops-service-connection/main.tf index 3751cd7..e9b47f4 100644 --- a/modules/azure-devops-service-connection/main.tf +++ b/modules/azure-devops-service-connection/main.tf @@ -26,6 +26,7 @@ resource "azuredevops_serviceendpoint_azurerm" "workspace_endpoint" { azurerm_spn_tenantid = data.azurerm_client_config.current.tenant_id azurerm_subscription_id = data.azurerm_client_config.current.subscription_id azurerm_subscription_name = data.azurerm_subscription.current.display_name + credentials { serviceprincipalid = var.service_principal_id serviceprincipalkey = var.service_principal_secret diff --git a/modules/azure-devops-service-connection/provider.tf b/modules/azure-devops-service-connection/provider.tf index 8242239..ab608e4 100644 --- a/modules/azure-devops-service-connection/provider.tf +++ b/modules/azure-devops-service-connection/provider.tf @@ -2,11 +2,11 @@ terraform { required_providers { azuredevops = { source = "microsoft/azuredevops" - version = ">=0.1.0" - } - azurerm = { - source = "hashicorp/azurerm" - version = ">= 2.50.0" + version = ">=0.2.0" } + # azurerm = { + # source = "hashicorp/azurerm" + # version = ">= 2.50.0" + # } } } diff --git a/modules/azure-resources/main.tf b/modules/azure-resources/main.tf index 2e206bf..ce2276c 100644 --- a/modules/azure-resources/main.tf +++ b/modules/azure-resources/main.tf @@ -18,7 +18,6 @@ resource "azurerm_storage_account" "storage" { location = azurerm_resource_group.workspace.location account_tier = "Standard" account_replication_type = "LRS" - allow_blob_public_access = false tags = var.tags } @@ -39,15 +38,6 @@ resource "azurerm_key_vault" "kv" { enable_rbac_authorization = true } -# ------------------ -# Service Principals -# ------------------ - -# module "workspace_sp" { -# source = "./../service-principal" -# name = "${local.name}-sp" -# } - # ----------------------- # RBAC - Role Assignments # ----------------------- @@ -109,4 +99,4 @@ resource "azurerm_role_assignment" "kv_team_devs" { # } # Why does it take up to 10 minutes for Key Vault RBAC to propagate? -# See https://docs.microsoft.com/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli#known-limits-and-performance \ No newline at end of file +# See https://docs.microsoft.com/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli#known-limits-and-performance diff --git a/modules/azure-resources/variables.tf b/modules/azure-resources/variables.tf index dc205e9..4aa82dc 100644 --- a/modules/azure-resources/variables.tf +++ b/modules/azure-resources/variables.tf @@ -49,12 +49,6 @@ variable "client_object_id" { variable "tags" { description = "Tags to apply to Azure Resources" type = map(string) - default = { - demo = "governance" - devops = "true" - oss = "terraform" - public = "true" - } } data "azurerm_client_config" "current" {} diff --git a/modules/cicd-setup/.terraform.lock.hcl b/modules/cicd-setup/.terraform.lock.hcl new file mode 100644 index 0000000..a04e304 --- /dev/null +++ b/modules/cicd-setup/.terraform.lock.hcl @@ -0,0 +1,42 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azuread" { + version = "2.22.0" + constraints = ">= 2.22.0" + hashes = [ + "h1:8AfMLW7SHmCPDODLKpx4QY0kNV4IZBrwDvfS/yAzpTI=", + "zh:062d84c514cd5015af60693ca4f3aece80d358fd7172951546eaba8093065c5b", + "zh:13749654ccd901408c74de2e1d7de43157044c4e739edcc0a66012a6cc6bba7a", + "zh:138c107f6aa554924a241806bca69248af1b7ce79ec93c6eef369886f33eef0a", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:33c656e07492808da0584717a3cd52377dff15ae0f1f5f411321b8de08a7693e", + "zh:4e08570e51742e717a914db5dd15c0a73cd1686e0c1f1a07123d3aa70cc00718", + "zh:4fef3aca24238cead0798d29196c9e2270622091897dba040c21500c2ddb4095", + "zh:614c60e3dfdd17b7d93b9355e057c825bb36e61f5bc25ccbc6550ff7bd726b65", + "zh:65d8789b8b088322d4e27ea6cd9935749980fe0a1b94e8e56f0cca35c34c394e", + "zh:823abd9bbd9f42bc4c5769be033bf734bb81bb20152b7e1c009a6234b849e5b6", + "zh:9c7ece6b3c65253bfef6ee29acc0cac033ec061bd6755c5496a7e5c17997c918", + "zh:fc0ff3e3104ee6e89c2fa3bf6c83ba698062e64165b60acfe7ad00f2161d1250", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.4.0" + constraints = ">= 3.4.0" + hashes = [ + "h1:xhN9eEt1oP9me06v9VsrqpbImlZV16NkHpHcKgpZ66k=", + "zh:4e9913fc3378436d19150c334e5906eafb83a4af3a270423cb7cdda94b27371f", + "zh:5b3d0cec2a600dc1f6633baa8fc36368c5c330fd7654861edcfa76f760a8f6a9", + "zh:5e0e1f899027bc182f31d996c9611e5ba27a034c848d7b0519b39e559fc4f38d", + "zh:66e3a1383ed6a0370989f6fd6abcfa63ccf6918ae535108595af57b9c20a9257", + "zh:688493baf6a116a399b737d74c11080051aca1ab087e5cddd14cc683b7e45c76", + "zh:9e471d85d52343e3ba778f3a94626d820fbec97bb589a3ac7a6a0939b9387770", + "zh:be1e85635daca1768f26962a4cbbadbf7fd13d9da8f9f188e938beca542c2ad5", + "zh:c00e14b6aa566eb9995cb0e1611a18fb8650d9f35c7636a7643a1b6e22660226", + "zh:c40711e5021838fd879da4c9e6b8f7e72104ada2adf0f3ba22e1cc32c3c54086", + "zh:cc62f8541de8d79577e57664e4f03c1fca893d455e5fb238d20668389c0f09ee", + "zh:cd9cbb5c6e5ceb5fcc7c4d0cab516ff209667d1b539b8c7436bd5e452c6aba8f", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/modules/cicd-setup/README.md b/modules/cicd-setup/README.md new file mode 100644 index 0000000..c7e83f0 --- /dev/null +++ b/modules/cicd-setup/README.md @@ -0,0 +1,65 @@ +# Initial Setup for CI/CD + +Many Azure Active Directory Objects including service principals require owners. To prevent errors in running future Infrastructure as Code and Azure Portal use, this script will bootstrap this initial group for you. + +❗️ This is not part of the main project. Thus you must manually navigate `cd` into this directory and run. Save the `aad_superowners_group_id` output for [deploying the main project demo](https://github.com/Azure/devops-governance/blob/main/DEPLOY.md). + +### Security Concerns + +- Do NOT use a production subcription because this code automates Azure AD objects, which are security concerns if not managed properly. +- Be aware that "Owner" assignments are a security risk. This demo uses owner because custom roles requires an [Azure AD Premium P1 or P2 license](https://docs.microsoft.com/en-us/azure/active-directory/roles/custom-create). +- For production scenarios, please read this project's accompanying Azure Architecture Center article about [best practices for custom "headless owner" roles](https://docs.microsoft.com/en-us/azure/architecture/example-scenario/governance/end-to-end-governance-in-azure#3-create-a-custom-role-for-the-service-principal-used-to-access-production). + +### Confirm you have required Azure AD Permissions + +Most code in this project will fail without proper permissions. Per [AAD Provider for Terraform Docs](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal#api-permissions)… + - A **Service Principal** needs one of the following *application roles* + - `Application.ReadWrite.All` + - or `Directory.ReadWrite.All` + - A **User Principal** needs one of the following *directory roles* + - `Application Administrator` + - or `Global Administrator` + +### Resources created + +When this Infrastructure as Code is deployed successfully… + + +…the following resources will be created: + +- **Service Principal** named `governance-demo-github-cicd` +- **Role Assignment** of `Owner` role to service principal at current subscription scope +- New **Azure AD group** named `governance-demo-subscription-owners` with memberships + - the current logged-in user + - service principal created above + +#### Example Terraform Output + +``` +aad_superowners_group_id = "73c74b2f-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +headless_owner_service_principal = { + "display_name" = "governance-demo-github-cicd" + "object_id" = "2c05b567-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + +### Required Object IDs for Main Project + +These values can be set locally. See [`local.auto.tfvars`](./../../../devops-governance/local.auto.tfvars.example) for details. + +👉 Note the **aad_superowners_group_id** value `73c74b2f-xxxx-xxxx-xxxx-xxxxxxxxxxxx` which you need for the `superadmins_aad_object_id` variable in the main project. + +👉 Note the **headless_owner_service_principal.object_id** value `2c05b567-xxxx-xxxx-xxxx-xxxxxxxxxxxx` which you need for the `application_owners_ids` variable in the main project. + + +## ❗️ Last Step - Grant Admin Consent + +The headless owner service principal will not work until you [*manually* grant "Admin content" via the Azure Portal](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent#grant-admin-consent-in-app-registrations +). + +This step is manual and not automated because you should read the docs, warnings, etc. before clicking that button and accepting the security risks. + +## References + +- Terraform Docs - [Azure AD Service Principal - Required API Permissions](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal#api-permissions) + diff --git a/modules/cicd-setup/main.tf b/modules/cicd-setup/main.tf new file mode 100644 index 0000000..9c8db61 --- /dev/null +++ b/modules/cicd-setup/main.tf @@ -0,0 +1,128 @@ +terraform { + required_providers { + azuread = { + source = "hashicorp/azuread" + version = ">=2.22.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = ">=3.4.0" + } + } + required_version = ">= 0.15" +} + +provider "azurerm" { + features {} +} + + +# ======== +# Config +# ======== + +locals { + headless_owner_name = "governance-demo-github-cicd" + owners_group_name = "governance-demo-subscription-owners" + tags = [ + "ci-managed:false", + "demo:true" + ] +} + +data "azurerm_subscription" "visual_studio" {} +data "azuread_client_config" "current" {} +data "azurerm_client_config" "current" {} + + +# ================================== +# Headless Owner Service Principal +# ================================== +# For App Role IDs, see +# https://docs.microsoft.com/en-us/graph/permissions-reference#all-permissions-and-ids + +resource "azuread_application" "headless_owner" { + display_name = local.headless_owner_name + owners = [data.azuread_client_config.current.object_id] + tags = local.tags + + required_resource_access { + resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph + + resource_access { + id = "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9" # Application.ReadWrite.All + type = "Role" + } + + resource_access { + id = "19dbc75e-c2e2-444c-a770-ec69d8559fc7" # Directory.ReadWrite.All + type = "Role" + } + } + + lifecycle { + ignore_changes = [ + owners + ] + } +} + +resource "azuread_service_principal" "headless_owner" { + application_id = azuread_application.headless_owner.application_id + app_role_assignment_required = false + owners = [data.azuread_client_config.current.object_id] + tags = local.tags + + lifecycle { + ignore_changes = [ + owners, + tags + ] + } +} + +resource "azuread_service_principal_password" "headless_owner" { + service_principal_id = azuread_service_principal.headless_owner.object_id +} + +# ================== +# Owners AAD Group +# ================== + +resource "azuread_group" "superowners" { + display_name = local.owners_group_name + security_enabled = true + prevent_duplicate_names = true + owners = [data.azuread_client_config.current.object_id] + members = [ + data.azuread_client_config.current.object_id, + azuread_service_principal.headless_owner.id + ] +} + + +# ============================= +# "Owner" ARM Role Assignment +# ============================= + +resource "azurerm_role_assignment" "gov_demo_owners_group" { + scope = data.azurerm_subscription.visual_studio.id + role_definition_name = "Owner" + principal_id = azuread_group.superowners.object_id +} + + +# ========= +# Outputs +# ========= + +output "aad_superowners_group_id" { + value = azuread_group.superowners.object_id +} + +output "headless_owner_service_principal" { + value = { + display_name = azuread_service_principal.headless_owner.display_name + object_id = azuread_service_principal.headless_owner.object_id + } +} diff --git a/modules/service-principal/main.tf b/modules/service-principal/main.tf index 6abfc17..0046218 100644 --- a/modules/service-principal/main.tf +++ b/modules/service-principal/main.tf @@ -4,13 +4,14 @@ resource "azuread_application" "app" { display_name = local.name - owners = var.owners + owners = var.owners_list } -resource "azuread_application_password" "workspace_sp_secret" { - application_object_id = azuread_application.app.object_id +resource "azuread_service_principal_password" "workspace_sp_secret" { + service_principal_id = azuread_service_principal.sp.object_id } resource "azuread_service_principal" "sp" { application_id = azuread_application.app.application_id + owners = var.owners_list } diff --git a/modules/service-principal/outputs.tf b/modules/service-principal/outputs.tf index 59f46b0..777ba66 100644 --- a/modules/service-principal/outputs.tf +++ b/modules/service-principal/outputs.tf @@ -20,7 +20,7 @@ output "client_id" { } output "client_secret" { - value = azuread_application_password.workspace_sp_secret.value + value = azuread_service_principal_password.workspace_sp_secret.value description = "Client Secret for Service Principal to be imported into Key Vault" sensitive = true } diff --git a/modules/service-principal/provider.tf b/modules/service-principal/provider.tf deleted file mode 100644 index 5495ee4..0000000 --- a/modules/service-principal/provider.tf +++ /dev/null @@ -1,13 +0,0 @@ -terraform { - required_providers { - azuread = { - source = "hashicorp/azuread" - version = ">=1.4.0" - } - - random = { - source = "hashicorp/random" - version = "~> 3.1.0" - } - } -} diff --git a/modules/service-principal/variables.tf b/modules/service-principal/variables.tf index c9b6b1b..ce50752 100644 --- a/modules/service-principal/variables.tf +++ b/modules/service-principal/variables.tf @@ -17,17 +17,11 @@ variable "tenant_id" { default = "Current Client Tenant ID" } -variable "password_lifetime" { - type = string - description = "Lifetime of password in hours, e.g. '720h'. Defaults to 6 months. After password expires, credential needs to be refreshed (but not deleted)." - default = "4380h" -} - -variable "owners" { +variable "owners_list" { type = list(string) description = "A set of object IDs of principals that will be granted ownership of the application (service principal)." validation { - condition = length(var.owners) > 0 + condition = length(var.owners_list) > 0 error_message = "Every Application must have an owner. Owners cannot be empty." } } diff --git a/provider.tf b/provider.tf index e4d190d..6279ebb 100644 --- a/provider.tf +++ b/provider.tf @@ -4,15 +4,15 @@ terraform { required_providers { azuread = { source = "hashicorp/azuread" - version = ">=1.5.1" + version = ">=2.22.0" } azuredevops = { source = "microsoft/azuredevops" - version = ">=0.1.8" + version = ">=0.2.0" } azurerm = { source = "hashicorp/azurerm" - version = ">=2.63.0" + version = ">=3.4.0" } random = { source = "hashicorp/random" diff --git a/terraform.tfvars b/terraform.tfvars index baf2e3c..358ec87 100644 --- a/terraform.tfvars +++ b/terraform.tfvars @@ -1,6 +1,6 @@ -# --------------- -# Azure AD Groups -# --------------- +# ================= +# Azure AD Groups +# ================= # Workspaces generally have 2 groups of actors, general # team members who are granted "Contributor" permissions # and admins who are granted "Owner" permissions. @@ -17,9 +17,9 @@ groups = { infra_admins = "infra-admins" } -# --------------------- -# Azure DevOps Projects -# --------------------- +# ======================= +# Azure DevOps Projects +# ======================= projects = { proj_fruits = { @@ -41,9 +41,9 @@ projects = { } } -# ---------------- -# ARM Environments -# ---------------- +# ================== +# ARM Environments +# ================== # The keys can be referenced in outputs, # e.g. module.workspace["shared"]. Suffixes are appended later. diff --git a/variables.tf b/variables.tf index 8ecdf7a..0899c43 100644 --- a/variables.tf +++ b/variables.tf @@ -5,6 +5,7 @@ variable "superadmins_aad_object_id" { default = "" } +# Service Principal Owners variable "application_owners_ids" { type = list(string) description = "A set of object IDs of principals that will be granted ownership of the application (service principal). Supported object types are users or service principals. It is best practice to specify one or more owners, incl. the principal used to execute Terraform" @@ -25,3 +26,20 @@ variable "projects" { variable "environments" { type = map(map(string)) } + +variable "default_tags" { + description = "Tags to apply to Azure Resources" + type = map(string) + default = { + public = "true" + demo = "e2e-governance" + iac = "terraform" + ci = "azure-pipelines" + } +} + +variable "custom_tags" { + description = "Extra Tags to apply to Azure Resources" + type = map(string) + default = {} +}