diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bf89629..950fd1e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,14 +18,24 @@ jobs: run: | terraform init terraform graph | dot -Tpng >graph.png - git push shell: bash working-directory: terraform/azure-devops/create-service-connection - name: Render terraform docs and push changes back to PR uses: terraform-docs/gh-actions@main with: + config-file: doc-gen/.terraform-docs.yml working-dir: terraform/azure-devops/create-service-connection - output-file: README.md - output-method: inject - git-push: "true" \ No newline at end of file + output-format: markdown table + git-push: false + + - name: Commit changes + run: | + git add graph.png + git add README.md + git config --local user.email "" + git config --local user.name "GitHub Actions" + git diff-index --quiet HEAD || git commit -m "Update docs" + git push + shell: bash + working-directory: terraform/azure-devops/create-service-connection \ No newline at end of file diff --git a/terraform/azure-devops/create-service-connection/.terraform.lock.hcl b/terraform/azure-devops/create-service-connection/.terraform.lock.hcl index dc451aa..3656db1 100644 --- a/terraform/azure-devops/create-service-connection/.terraform.lock.hcl +++ b/terraform/azure-devops/create-service-connection/.terraform.lock.hcl @@ -23,22 +23,22 @@ provider "registry.terraform.io/hashicorp/azuread" { } provider "registry.terraform.io/hashicorp/azurerm" { - version = "3.99.0" + version = "3.101.0" constraints = "~> 3.66" hashes = [ - "h1:dawmYJUMGlL3t1mKDyaLJc08uSxPaUBoCAb/YCbVxPM=", - "h1:yHNaEhlR3kqlItAXFLWlIH2xxu4i7r2XzQnS04f/qBo=", - "zh:20581c1f4c586a37af45ed4c2a86ff4d868cee79139a755bd29750d804cee3ef", - "zh:28b3cc4e5f8bc65a595eab011d5965203a39e92aa9e26df842ffc979305ac823", - "zh:4cb167f8bb82f9065b7b50d012be3045fce3c699b0ea0e257ad1995441227f72", - "zh:6fa5c6fa430921a4e0fe8d44eaf12210fb90afdf3f83cedfde1c691ae36e953c", - "zh:75eff5b0ea9fca46ed5a0425c5e33fbda470e6448917817e80ae898688568665", - "zh:9af0aeaa74bfc764c60eec7d212d31deb70e03e970d22449f11170f75108f9cf", - "zh:b5055767199a2927d41b543a16e905c1e0b209f14a2144c756786194e133b41d", - "zh:c3e30b0eed068a148498ac78a9e013bc2eef0eb3cc3b4484f77421d64a797dc2", - "zh:ce87cd35cef9e5805f921978a91a7a4e139e8cbc7674a94076cb1a20a0c2feb1", - "zh:d87b84f144c865145bd10093ead99b653ea363fd4e7315675727659ca78544d0", - "zh:ee5900a50d69e046aab6581f6d888014b3f8d543e5b17c50761579d3370935f2", + "h1:EBBVeOrjBsTGX/0l3GCBoCk0K04MtKgJ36Sf4jwPh9g=", + "h1:Jrkhx+qKaf63sIV/WvE8sPR53QuC16pvTrBjxFVMPYM=", + "zh:38b02bce5cbe83f938a71716bbf9e8b07fed8b2c6b83c19b5e708eda7dee0f1d", + "zh:3ed094366ab35c4fcd632471a7e45a84ca6c72b00477cdf1276e541a0171b369", + "zh:62bf7cde429f465173e40eebb6840f4e380dfc9dcec2d89dbcb6ce5bce379e50", + "zh:90761096666575f0a21275822011e08d72389a575f45e4c1c8e1d26c3b794750", + "zh:9494acbacc2b67cf87ae510862ca2c826d0e04662274477f8de1707cefa7c0f3", + "zh:9a01128004eab67ed90e9decb92271c187e95e0d6e9f136b5bbc8bf3a2189d41", + "zh:9e4eed599cecc2b2aff4dc334b154aad0ad80b5a07439139fc28b22fcff0c8aa", + "zh:a5f940e5b8b813b18d9ecd974fdda1ae989870a8a5d897fda8cff4c5368e6e24", + "zh:bc7c6bfad523f6c0fad7ef9f8d4c264f72cb9f29fce3a69f8483c63e70eb5085", + "zh:d9ba2c6bd082775e6d2d6453486ebb3ecc86ecf127e1d86eddf1a952b545c04e", + "zh:e288cce3c324a26d1e01a83e3fe2215537075ab897364539b6cabba298122654", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } @@ -86,23 +86,23 @@ provider "registry.terraform.io/hashicorp/http" { } provider "registry.terraform.io/hashicorp/random" { - version = "3.6.0" + version = "3.6.1" constraints = "~> 3.5" hashes = [ - "h1:I8MBeauYA8J8yheLJ8oSMWqB0kovn16dF/wKZ1QTdkk=", - "h1:R5Ucn26riKIEijcsiOMBR3uOAjuOMfI1x7XvH4P6B1w=", - "zh:03360ed3ecd31e8c5dac9c95fe0858be50f3e9a0d0c654b5e504109c2159287d", - "zh:1c67ac51254ba2a2bb53a25e8ae7e4d076103483f55f39b426ec55e47d1fe211", - "zh:24a17bba7f6d679538ff51b3a2f378cedadede97af8a1db7dad4fd8d6d50f829", - "zh:30ffb297ffd1633175d6545d37c2217e2cef9545a6e03946e514c59c0859b77d", - "zh:454ce4b3dbc73e6775f2f6605d45cee6e16c3872a2e66a2c97993d6e5cbd7055", + "h1:1OlP753r4lOKlBprL0HdZGWerm5DCabD5Mli8k8lWAg=", + "h1:a+Goawwh6Qtg4/bRWzfDtIdrEFfPlnVy0y4LdUQY3nI=", + "zh:2a0ec154e39911f19c8214acd6241e469157489fc56b6c739f45fbed5896a176", + "zh:57f4e553224a5e849c99131f5e5294be3a7adcabe2d867d8a4fef8d0976e0e52", + "zh:58f09948c608e601bd9d0a9e47dcb78e2b2c13b4bda4d8f097d09152ea9e91c5", + "zh:5c2a297146ed6fb3fe934c800e78380f700f49ff24dbb5fb5463134948e3a65f", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:91df0a9fab329aff2ff4cf26797592eb7a3a90b4a0c04d64ce186654e0cc6e17", - "zh:aa57384b85622a9f7bfb5d4512ca88e61f22a9cea9f30febaa4c98c68ff0dc21", - "zh:c4a3e329ba786ffb6f2b694e1fd41d413a7010f3a53c20b432325a94fa71e839", - "zh:e2699bc9116447f96c53d55f2a00570f982e6f9935038c3810603572693712d0", - "zh:e747c0fd5d7684e5bfad8aa0ca441903f15ae7a98a737ff6aca24ba223207e2c", - "zh:f1ca75f417ce490368f047b63ec09fd003711ae48487fba90b4aba2ccf71920e", + "zh:7ce41e26f0603e31cdac849085fc99e5cd5b3b73414c6c6d955c0ceb249b593f", + "zh:8c9e8d30c4ef08ee8bcc4294dbf3c2115cd7d9049c6ba21422bd3471d92faf8a", + "zh:93e91be717a7ffbd6410120eb925ebb8658cc8f563de35a8b53804d33c51c8b0", + "zh:982542e921970d727ce10ed64795bf36c4dec77a5db0741d4665230d12250a0d", + "zh:b9d1873f14d6033e216510ef541c891f44d249464f13cc07d3f782d09c7d18de", + "zh:cfe27faa0bc9556391c8803ade135a5856c34a3fe85b9ae3bdd515013c0c87c1", + "zh:e4aabf3184bbb556b89e4b195eab1514c86a2914dd01c23ad9813ec17e863a8a", ] } diff --git a/terraform/azure-devops/create-service-connection/README.md b/terraform/azure-devops/create-service-connection/README.md index 0d7cbe3..462fd2e 100644 --- a/terraform/azure-devops/create-service-connection/README.md +++ b/terraform/azure-devops/create-service-connection/README.md @@ -1,5 +1,7 @@ -# Terraform managed Azure Service Connection +# Terraform-managed Azure Service Connection + +[![Build Status](https://dev.azure.com/geekzter/Pipeline%20Playground/_apis/build/status%2Fcreate-service-connection?branchName=main&label=terraform-ci)](https://dev.azure.com/geekzter/Pipeline%20Playground/_build/latest?definitionId=5&branchName=main) Many large customers have additional requirements around the management of the Entra ID object that a service connection creates and the permissions it is assigned to. @@ -12,6 +14,8 @@ These are a few common requirements and constraints: - Co-owners are required to exist for Entra ID app registrations - The organization has an IT fulfillment process where identities are automatically created based on a service request +## Why Terraform? + Terraform employs a provider model which enable all changes to be made by a single tool and configuration: | Service | Provider | API | @@ -20,61 +24,125 @@ Terraform employs a provider model which enable all changes to be made by a sing | Azure DevOps | [azuredevops](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs) | [Azure DevOps REST API](https://learn.microsoft.com/rest/api/azure/devops/serviceendpoint/endpoints) | | Entra ID | [azuread](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs) | [Microsoft Graph API](https://learn.microsoft.com/graph/use-the-api) | -## Provisioning - Terraform is a declarative tool that is capable if inferring dependencies to create resources in the correct order. This is the output from `terraform graph`: ![Terraform graph](graph.png) -Provisioning is a matter of specifying [variables](https://developer.hashicorp.com/terraform/language/values/variables) (see [inputs](#input_azdo_organization_url) below) and running `terraform apply`. +More information: + +- [Overview of Terraform on Azure - What is Terraform?](https://learn.microsoft.com/azure/developer/terraform/overview) +- [Cloud Adoption Framework Infrastructure-as-Code CI/CD security guidance](https://learn.microsoft.com/azure/cloud-adoption-framework/secure/best-practices/secure-devops) -- To understand how the Terraform configuration can be created in automation, review +## Provisioning + +Provisioning is a matter of specifying [variables](https://developer.hashicorp.com/terraform/language/values/variables) (see [inputs](#input_azdo_organization_url) below) and running `terraform apply`. To understand how the Terraform configuration can be created in automation, review [tf_create_azurerm_service_connection.ps1](../../../scripts/azure-devops/tf_create_azurerm_service_connection.ps1) and the [CI pipeline](azure-pipelines.yml). -- For more information on using Terraform with Azure and other Microsoft services, see [Overview of Terraform on Azure - What is Terraform?](https://learn.microsoft.com/azure/developer/terraform/overview) -- For infrastructure-as-code best practices, review [Securing the pipeline and CI/CD workflow](https://learn.microsoft.com/azure/cloud-adoption-framework/secure/best-practices/secure-devops). + +### Examples + +Terraform variable can be provided as a .auto.tfvars file, see [sample](config.auto.tfvars.sample). + +#### App registration with Federated Credential and ITSM data + +```hcl +azdo_creates_identity = false +azure_role_assignments = [ + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" + role = "Contributor" + }, + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "Storage Blob Data Contributor" + } +] +azdo_organization_url = "https://dev.azure.com/my-organization" +azdo_project_name = "my-project" +create_federation = true +create_managed_identity = false +entra_owner_object_ids = ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111"] +entra_service_management_reference = "11111111-1111-1111-1111-111111111111" +``` + +#### App registration with short-lived secret + +```hcl +azdo_creates_identity = false +azdo_organization_url = "https://dev.azure.com/my-organization" +azdo_project_name = "my-project" +azure_role_assignments = [ + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "Reader" + } +] +create_federation = false +create_managed_identity = false +entra_secret_expiration_days = 0 # secret lasts 1 hour +``` + +#### Managed Identity with Federated Credential + +```hcl +azdo_creates_identity = false +azdo_organization_url = "https://dev.azure.com/my-organization" +azdo_project_name = "my-project" +azure_role_assignments = [ + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" + role = "Contributor" + }, + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "Key Vault Secrets User" + } +] +create_federation = true +create_managed_identity = true +managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/msi-rg" +``` ## Terraform Configuration -## Providers +Generated with [terraform-docs](https://terraform-docs.io/). + +### Providers | Name | Version | |------|---------| -| [azurerm](#provider_azurerm) | 3.99.0 | +| [azurerm](#provider_azurerm) | 3.101.0 | | [external](#provider_external) | 2.3.3 | -| [random](#provider_random) | 3.6.0 | +| [random](#provider_random) | 3.6.1 | | [terraform](#provider_terraform) | n/a | -## Modules +### Modules | Name | Source | Version | |------|--------|---------| -| [azure_access](#module_azure_access) | ./modules/azure-access | n/a | | [azure_role_assignments](#module_azure_role_assignments) | ./modules/azure-access | n/a | | [entra_app](#module_entra_app) | ./modules/app-registration | n/a | | [managed_identity](#module_managed_identity) | ./modules/managed-identity | n/a | | [service_connection](#module_service_connection) | ./modules/service-connection | n/a | -## Inputs - -| Name | Description | Type | -|------|-------------|------| -| [azdo_organization_url](#input_azdo_organization_url) | The Azure DevOps organization URL (e.g. https://dev.azure.com/contoso) | `string` | -| [azdo_project_name](#input_azdo_project_name) | The Azure DevOps project name to create the service connection in | `string` | -| [azdo_creates_identity](#input_azdo_creates_identity) | Let Azure DevOps create identity for service connection | `bool` | -| [azure_role](#input_azure_role) | The Azure RBAC role to assign to the service connection's identity | `string` | -| [azure_role_assignments](#input_azure_role_assignments) | Additional role assignments to create for the service connection's identity | `set(object({scope=string, role=string}))` | -| [azure_scope](#input_azure_scope) | The Azure scope to assign access to | `string` | -| [create_federation](#input_create_federation) | Use workload identity federation instead of a App Registration secret | `bool` | -| [create_managed_identity](#input_create_managed_identity) | Creates a Managed Identity instead of a App Registration | `bool` | -| [entra_owner_object_ids](#input_entra_owner_object_ids) | Object ids of the users that will be co-owners of the Entra ID app registration | `list(string)` | -| [entra_secret_expiration_days](#input_entra_secret_expiration_days) | Secret expiration in days | `number` | -| [entra_service_management_reference](#input_entra_service_management_reference) | IT Service Management Reference to add to the App Registration | `string` | -| [managed_identity_resource_group_id](#input_managed_identity_resource_group_id) | The resource group to create the Managed Identity in | `string` | -| [resource_prefix](#input_resource_prefix) | The prefix to put in front of resource names created | `string` | -| [resource_suffix](#input_resource_suffix) | The suffix to append to resource names created | `string` | -| [run_id](#input_run_id) | The ID that identifies the pipeline / workflow that invoked Terraform (used in CI/CD) | `number` | - -## Outputs +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azdo_organization_url](#input_azdo_organization_url) | The Azure DevOps organization URL (e.g. https://dev.azure.com/contoso) | `string` | n/a | yes | +| [azdo_project_name](#input_azdo_project_name) | The Azure DevOps project name to create the service connection in | `string` | n/a | yes | +| [azdo_creates_identity](#input_azdo_creates_identity) | Let Azure DevOps create identity for service connection | `bool` | `false` | no | +| [azure_role_assignments](#input_azure_role_assignments) | Role assignments to create for the service connection's identity. If this is empty, the Contributor role will be assigned on the azurerm provider subscription. | `set(object({scope=string, role=string}))` | `[]` | no | +| [create_federation](#input_create_federation) | Use workload identity federation instead of a App Registration secret | `bool` | `true` | no | +| [create_managed_identity](#input_create_managed_identity) | Creates a Managed Identity instead of a App Registration | `bool` | `true` | no | +| [entra_owner_object_ids](#input_entra_owner_object_ids) | Object ids of the users that will be co-owners of the Entra ID app registration | `list(string)` | `null` | no | +| [entra_secret_expiration_days](#input_entra_secret_expiration_days) | Secret expiration in days | `number` | `90` | no | +| [entra_service_management_reference](#input_entra_service_management_reference) | IT Service Management Reference to add to the App Registration | `string` | `null` | no | +| [managed_identity_resource_group_id](#input_managed_identity_resource_group_id) | The resource group to create the Managed Identity in | `string` | `null` | no | +| [resource_prefix](#input_resource_prefix) | The prefix to put in front of resource names created | `string` | `"demo"` | no | +| [resource_suffix](#input_resource_suffix) | The suffix to append to resource names created | `string` | `""` | no | +| [run_id](#input_run_id) | The ID that identifies the pipeline / workflow that invoked Terraform (used in CI/CD) | `number` | `null` | no | + +### Outputs | Name | Description | |------|-------------| @@ -82,9 +150,7 @@ Provisioning is a matter of specifying [variables](https://developer.hashicorp.c | [azdo_service_connection_id](#output_azdo_service_connection_id) | The Azure DevOps service connection id | | [azdo_service_connection_name](#output_azdo_service_connection_name) | The Azure DevOps service connection name | | [azdo_service_connection_url](#output_azdo_service_connection_url) | The Azure DevOps service connection portal URL | -| [azure_resource_group_name](#output_azure_resource_group_name) | The name of the resource group the service connection was granted access to | -| [azure_scope](#output_azure_scope) | The Azure scope the service connection was granted access to | -| [azure_scope_url](#output_azure_scope_url) | The Azure scope portal URL the service connection was granted access to | +| [azure_role_assignments](#output_azure_role_assignments) | Role assignments created for the service connection's identity | | [azure_subscription_id](#output_azure_subscription_id) | The Azure subscription id the service connection was granted access to | | [azure_subscription_name](#output_azure_subscription_name) | The Azure subscription name the service connection was granted access to | | [identity_application_id](#output_identity_application_id) | The app/client id of the service connection's identity | diff --git a/terraform/azure-devops/create-service-connection/azure-pipelines.yml b/terraform/azure-devops/create-service-connection/azure-pipelines.yml index 9f8cba9..dd26ae2 100644 --- a/terraform/azure-devops/create-service-connection/azure-pipelines.yml +++ b/terraform/azure-devops/create-service-connection/azure-pipelines.yml @@ -19,7 +19,7 @@ parameters: - name: concurrency displayName: Concurrency type: number - default: 2 + default: 1 - name: testServiceConnection displayName: Test Service Connection(s) type: boolean diff --git a/terraform/azure-devops/create-service-connection/config.auto.tfvars.sample b/terraform/azure-devops/create-service-connection/config.auto.tfvars.sample new file mode 100755 index 0000000..50d5798 --- /dev/null +++ b/terraform/azure-devops/create-service-connection/config.auto.tfvars.sample @@ -0,0 +1,30 @@ +# Rename to .auto.tfvars to process automatically + +azdo_creates_identity = false +azdo_organization_url = "https://dev.azure.com/myorg" +azdo_project_name = "my-organization" +azure_role_assignments = [ + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" + role = "Contributor" + }, + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "AcrPush" + }, + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "Key Vault Secrets User" + }, + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "Storage Blob Data Contributor" + } +] +create_federation = true +create_managed_identity = true +entra_owner_object_ids = ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111"] +entra_secret_expiration_days = 0 +entra_service_management_reference = "11111111-1111-1111-1111-111111111111" +managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-service-connections" +resource_prefix = "myalias" diff --git a/terraform/azure-devops/create-service-connection/.terraform-docs.yml b/terraform/azure-devops/create-service-connection/doc-gen/.terraform-docs.yml similarity index 91% rename from terraform/azure-devops/create-service-connection/.terraform-docs.yml rename to terraform/azure-devops/create-service-connection/doc-gen/.terraform-docs.yml index 662ce40..a98dc3d 100644 --- a/terraform/azure-devops/create-service-connection/.terraform-docs.yml +++ b/terraform/azure-devops/create-service-connection/doc-gen/.terraform-docs.yml @@ -58,10 +58,10 @@ sections: # # {{ .Inputs }} -# # see: https://terraform-docs.io/user-guide/configuration/output -# output: -# file: README.md -# mode: inject +# see: https://terraform-docs.io/user-guide/configuration/output +output: + file: README.md + mode: inject # template: |- # # The template can be customized with aribitrary markdown content. @@ -85,8 +85,10 @@ sort: # see: https://terraform-docs.io/user-guide/configuration/settings settings: - indent: 4 + indent: 3 escape: false - default: false - required: false + default: true + lockfile: true + required: true + sensitive: true type: true \ No newline at end of file diff --git a/terraform/azure-devops/create-service-connection/doc-gen/header.md b/terraform/azure-devops/create-service-connection/doc-gen/header.md index f84be32..840db54 100644 --- a/terraform/azure-devops/create-service-connection/doc-gen/header.md +++ b/terraform/azure-devops/create-service-connection/doc-gen/header.md @@ -1,4 +1,6 @@ -# Terraform managed Azure Service Connection +# Terraform-managed Azure Service Connection + +[![Build Status](https://dev.azure.com/geekzter/Pipeline%20Playground/_apis/build/status%2Fcreate-service-connection?branchName=main&label=terraform-ci)](https://dev.azure.com/geekzter/Pipeline%20Playground/_build/latest?definitionId=5&branchName=main) Many large customers have additional requirements around the management of the Entra ID object that a service connection creates and the permissions it is assigned to. @@ -11,6 +13,8 @@ These are a few common requirements and constraints: - Co-owners are required to exist for Entra ID app registrations - The organization has an IT fulfillment process where identities are automatically created based on a service request +## Why Terraform? + Terraform employs a provider model which enable all changes to be made by a single tool and configuration: | Service | Provider | API | @@ -19,19 +23,84 @@ Terraform employs a provider model which enable all changes to be made by a sing | Azure DevOps | [azuredevops](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs) | [Azure DevOps REST API](https://learn.microsoft.com/rest/api/azure/devops/serviceendpoint/endpoints) | | Entra ID | [azuread](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs) | [Microsoft Graph API](https://learn.microsoft.com/graph/use-the-api) | - -## Provisioning - Terraform is a declarative tool that is capable if inferring dependencies to create resources in the correct order. This is the output from `terraform graph`: ![Terraform graph](graph.png) -Provisioning is a matter of specifying [variables](https://developer.hashicorp.com/terraform/language/values/variables) (see [inputs](#input_azdo_organization_url) below) and running `terraform apply`. +More information: + +- [Overview of Terraform on Azure - What is Terraform?](https://learn.microsoft.com/azure/developer/terraform/overview) +- [Cloud Adoption Framework Infrastructure-as-Code CI/CD security guidance](https://learn.microsoft.com/azure/cloud-adoption-framework/secure/best-practices/secure-devops) + +## Provisioning -- To understand how the Terraform configuration can be created in automation, review +Provisioning is a matter of specifying [variables](https://developer.hashicorp.com/terraform/language/values/variables) (see [inputs](#input_azdo_organization_url) below) and running `terraform apply`. To understand how the Terraform configuration can be created in automation, review [tf_create_azurerm_service_connection.ps1](../../../scripts/azure-devops/tf_create_azurerm_service_connection.ps1) and the [CI pipeline](azure-pipelines.yml). -- For more information on using Terraform with Azure and other Microsoft services, see [Overview of Terraform on Azure - What is Terraform?](https://learn.microsoft.com/azure/developer/terraform/overview) -- For infrastructure-as-code best practices, review [Securing the pipeline and CI/CD workflow](https://learn.microsoft.com/azure/cloud-adoption-framework/secure/best-practices/secure-devops). +### Examples + +Terraform variable can be provided as a .auto.tfvars file, see [sample](config.auto.tfvars.sample). + +#### App registration with Federated Credential and ITSM data + +```hcl +azdo_creates_identity = false +azure_role_assignments = [ + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" + role = "Contributor" + }, + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "Storage Blob Data Contributor" + } +] +azdo_organization_url = "https://dev.azure.com/my-organization" +azdo_project_name = "my-project" +create_federation = true +create_managed_identity = false +entra_owner_object_ids = ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111"] +entra_service_management_reference = "11111111-1111-1111-1111-111111111111" +``` + +#### App registration with short-lived secret + +```hcl +azdo_creates_identity = false +azdo_organization_url = "https://dev.azure.com/my-organization" +azdo_project_name = "my-project" +azure_role_assignments = [ + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "Reader" + } +] +create_federation = false +create_managed_identity = false +entra_secret_expiration_days = 0 # secret lasts 1 hour +``` + +#### Managed Identity with Federated Credential + +```hcl +azdo_creates_identity = false +azdo_organization_url = "https://dev.azure.com/my-organization" +azdo_project_name = "my-project" +azure_role_assignments = [ + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" + role = "Contributor" + }, + { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg" + role = "Key Vault Secrets User" + } +] +create_federation = true +create_managed_identity = true +managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/msi-rg" +``` + +## Terraform Configuration -## Terraform Configuration \ No newline at end of file +Generated with [terraform-docs](https://terraform-docs.io/). \ No newline at end of file diff --git a/terraform/azure-devops/create-service-connection/graph.png b/terraform/azure-devops/create-service-connection/graph.png index f440f36..25c54b4 100644 Binary files a/terraform/azure-devops/create-service-connection/graph.png and b/terraform/azure-devops/create-service-connection/graph.png differ diff --git a/terraform/azure-devops/create-service-connection/main.tf b/terraform/azure-devops/create-service-connection/main.tf index 9df0869..07b5935 100644 --- a/terraform/azure-devops/create-service-connection/main.tf +++ b/terraform/azure-devops/create-service-connection/main.tf @@ -1,4 +1,8 @@ data azurerm_client_config current {} +data azurerm_subscription current {} +data azurerm_subscription target { + subscription_id = split("/",tolist(local.azure_role_assignments)[0].scope)[2] +} # Random resource suffix, this will prevent name collisions when creating resources in parallel resource random_string suffix { @@ -15,8 +19,16 @@ locals { azdo_organization_name = split("/",var.azdo_organization_url)[3] azdo_organization_url = replace(var.azdo_organization_url,"/\\/$/","") azdo_project_url = "${local.azdo_organization_url}/${urlencode(var.azdo_project_name)}" - azdo_service_connection_name = "${replace(module.azure_access.subscription_name,"/ +/","-")}-${var.azdo_creates_identity ? "aut" : "man"}-${var.create_managed_identity ? "msi" : "sp"}-${var.create_federation ? "oidc" : "secret"}${terraform.workspace == "default" ? "" : format("-%s",terraform.workspace)}-${local.resource_suffix}" - azure_scope = var.azure_scope != null && var.azure_scope != "" ? var.azure_scope : "/subscriptions/${data.azurerm_client_config.current.subscription_id}" + # azdo_service_connection_name = "${replace(data.azurerm_subscription.target.display_name,"/ +/","-")}-${var.azdo_creates_identity ? "aut" : "man"}-${var.create_managed_identity ? "msi" : "sp"}-${var.create_federation ? "oidc" : "secret"}${terraform.workspace == "default" ? "" : format("-%s",terraform.workspace)}-${local.resource_suffix}" + azdo_service_connection_name = "${replace(data.azurerm_subscription.target.display_name,"/ +/","-")}${terraform.workspace == "default" ? "" : format("-%s",terraform.workspace)}-${local.resource_suffix}" + azure_role_assignments = length(var.azure_role_assignments) > 0 ? var.azure_role_assignments : [ + { + # Default role assignment + role = "Contributor" + scope = data.azurerm_subscription.current.id + } + ] + managed_identity_subscription_id = var.create_managed_identity ? split("/", var.managed_identity_resource_group_id)[2] : null principal_id = var.azdo_creates_identity ? null : (var.create_managed_identity ? module.managed_identity.0.principal_id : module.entra_app.0.principal_id) principal_name = var.azdo_creates_identity ? null : (var.create_managed_identity ? module.managed_identity.0.principal_name : module.entra_app.0.principal_name) resource_suffix = var.resource_suffix != null && var.resource_suffix != "" ? lower(var.resource_suffix) : random_string.suffix.result @@ -30,8 +42,6 @@ locals { runId = var.run_id workspace = terraform.workspace } - managed_identity_subscription_id = var.create_managed_identity ? split("/", var.managed_identity_resource_group_id)[2] : null - target_subscription_id = split("/", local.azure_scope)[2] } resource terraform_data managed_identity_validator { @@ -80,18 +90,6 @@ module entra_app { count = var.create_managed_identity || var.azdo_creates_identity ? 0 : 1 } -module azure_access { - providers = { - azurerm = azurerm.target - } - source = "./modules/azure-access" - # create_role_assignment = !var.azdo_creates_identity - create_role_assignment = true - identity_object_id = local.principal_id - resource_id = local.azure_scope - role = var.azure_role -} - module azure_role_assignments { providers = { azurerm = azurerm.target @@ -102,7 +100,7 @@ module azure_role_assignments { resource_id = each.value.scope role = each.value.role - for_each = { for ra in var.azure_role_assignments : format("%s-%s", ra.scope, ra.role) => ra } + for_each = { for ra in local.azure_role_assignments : format("%s-%s", ra.scope, ra.role) => ra } } module service_connection { @@ -114,6 +112,6 @@ module service_connection { project_name = var.azdo_project_name tenant_id = data.azurerm_client_config.current.tenant_id service_connection_name = local.azdo_service_connection_name - subscription_id = local.target_subscription_id - subscription_name = module.azure_access.subscription_name + subscription_id = data.azurerm_subscription.target.subscription_id + subscription_name = data.azurerm_subscription.target.display_name } diff --git a/terraform/azure-devops/create-service-connection/outputs.tf b/terraform/azure-devops/create-service-connection/outputs.tf index 6fd42f8..31339fe 100644 --- a/terraform/azure-devops/create-service-connection/outputs.tf +++ b/terraform/azure-devops/create-service-connection/outputs.tf @@ -14,25 +14,17 @@ output azdo_service_connection_url { description = "The Azure DevOps service connection portal URL" value = module.service_connection.service_connection_url } -output azure_resource_group_name { - description = "The name of the resource group the service connection was granted access to" - value = try(split("/", local.azure_scope)[4],null) -} -output azure_scope { - description = "The Azure scope the service connection was granted access to" - value = local.azure_scope -} -output azure_scope_url { - description = "The Azure scope portal URL the service connection was granted access to" - value = module.azure_access.resource_url +output azure_role_assignments { + description = "Role assignments created for the service connection's identity" + value = local.azure_role_assignments } output azure_subscription_id { description = "The Azure subscription id the service connection was granted access to" - value = local.target_subscription_id + value = data.azurerm_subscription.target.subscription_id } output azure_subscription_name { description = "The Azure subscription name the service connection was granted access to" - value = module.azure_access.subscription_name + value = data.azurerm_subscription.target.display_name } output identity_application_id { diff --git a/terraform/azure-devops/create-service-connection/terraform.tf b/terraform/azure-devops/create-service-connection/terraform.tf index 7180134..7226977 100644 --- a/terraform/azure-devops/create-service-connection/terraform.tf +++ b/terraform/azure-devops/create-service-connection/terraform.tf @@ -53,5 +53,5 @@ provider azurerm { prevent_deletion_if_contains_resources = false } } - subscription_id = local.target_subscription_id + subscription_id = data.azurerm_subscription.target.subscription_id } \ No newline at end of file diff --git a/terraform/azure-devops/create-service-connection/variables.tf b/terraform/azure-devops/create-service-connection/variables.tf index db8bc67..e6a2f7c 100644 --- a/terraform/azure-devops/create-service-connection/variables.tf +++ b/terraform/azure-devops/create-service-connection/variables.tf @@ -15,23 +15,10 @@ variable azdo_project_name { type = string } -variable azure_scope { - default = null - description = "The Azure scope to assign access to" - type = string -} - -variable azure_role { - default = "Contributor" - description = "The Azure RBAC role to assign to the service connection's identity" - nullable = false - type = string -} - variable azure_role_assignments { default = [] - description = "Additional role assignments to create for the service connection's identity" - nullable = true + description = "Role assignments to create for the service connection's identity. If this is empty, the Contributor role will be assigned on the azurerm provider subscription." + nullable = false type = set(object({scope=string, role=string})) }