From 0f3d72274a6168057bf529b198f49704e23ecd73 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 4 Nov 2025 15:05:44 +0100 Subject: [PATCH 1/5] fix: service connection now also creates federated credentials --- .../backplane/README.md | 38 ++++------------- .../backplane/main.tf | 9 ---- .../backplane/outputs.tf | 11 ++--- .../backplane/variables.tf | 15 ------- .../buildingblock/APP_TEAM_README.md | 15 ++++--- .../buildingblock/README.md | 42 +++++++++++-------- .../buildingblock/main.tf | 9 ++++ .../buildingblock/variables.tf | 5 +++ .../buildingblock/versions.tf | 4 ++ 9 files changed, 63 insertions(+), 85 deletions(-) diff --git a/modules/azuredevops/service-connection-subscription/backplane/README.md b/modules/azuredevops/service-connection-subscription/backplane/README.md index a29f930..c6a85d7 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/README.md +++ b/modules/azuredevops/service-connection-subscription/backplane/README.md @@ -4,11 +4,12 @@ This module provisions the infrastructure required to support the Azure DevOps S ## What It Provisions -- **Azure AD Service Principal**: For service connection management automation +- **Azure AD Application and Service Principal**: For service connection management automation - **Azure Key Vault**: Stores Azure DevOps Personal Access Token (PAT) - **Custom Role Definition**: Minimal permissions for reading Key Vault secrets - **Role Assignment**: Grants the service principal access to Key Vault -- **Federated Identity Credential** (optional): For workload identity federation (OIDC) authentication + +**Note**: Federated identity credentials are now created automatically by the building block module using the actual service connection details from Azure DevOps. ## Prerequisites @@ -21,7 +22,7 @@ This module provisions the infrastructure required to support the Azure DevOps S ## Usage -### Basic Backplane (Service Principal Authentication) +### Basic Backplane ```hcl module "azuredevops_service_connection_backplane" { @@ -36,25 +37,6 @@ module "azuredevops_service_connection_backplane" { } ``` -### Backplane with Workload Identity Federation - -```hcl -module "azuredevops_service_connection_backplane" { - source = "./backplane" - - azure_devops_organization_url = "https://dev.azure.com/myorg" - service_principal_name = "azuredevops-serviceconn-terraform" - key_vault_name = "kv-azdo-sc-prod" - resource_group_name = "rg-azdo-sc-prod" - location = "West Europe" - scope = "/subscriptions/00000000-0000-0000-0000-000000000000" - enable_workload_identity_federation = true - azure_devops_organization_id = "33333333-3333-3333-3333-333333333333" - azure_devops_project_name = "MyProject" - service_connection_name = "Azure-Production-Federated" -} -``` - ## Post-Deployment Steps 1. Create an Azure DevOps PAT with `Service Connections (Read, Query & Manage)` scope @@ -65,9 +47,9 @@ module "azuredevops_service_connection_backplane" { ## Workload Identity Federation -When `enable_workload_identity_federation = true`, this module configures: -- **Issuer**: `https://vstoken.dev.azure.com/{organization_id}` -- **Subject**: `sc://{org_url}/{project}/{connection_name}` +Federated identity credentials are automatically created by the building block module after the service connection is provisioned. This ensures the issuer and subject values exactly match what Azure DevOps generates: +- **Issuer**: Automatically determined by Azure DevOps +- **Subject**: Automatically determined based on service connection details - **Audience**: `api://AzureADTokenExchange` This eliminates the need for client secrets by using OIDC token exchange. @@ -110,23 +92,19 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [azure\_devops\_organization\_id](#input\_azure\_devops\_organization\_id) | Azure DevOps organization ID (GUID) for workload identity federation | `string` | n/a | yes | | [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | -| [azure\_devops\_project\_name](#input\_azure\_devops\_project\_name) | Azure DevOps project name for workload identity federation | `string` | n/a | yes | | [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault to store the Azure DevOps PAT | `string` | n/a | yes | | [location](#input\_location) | Azure region for resources | `string` | `"West Europe"` | no | | [resource\_group\_name](#input\_resource\_group\_name) | Resource group name for the Key Vault | `string` | n/a | yes | | [scope](#input\_scope) | Azure scope for role definitions (subscription or management group) | `string` | n/a | yes | -| [service\_connection\_name](#input\_service\_connection\_name) | Azure DevOps service connection name for workload identity federation | `string` | n/a | yes | | [service\_principal\_name](#input\_service\_principal\_name) | Name for the Azure DevOps service principal | `string` | `"azure-devops-terraform"` | no | ## Outputs | Name | Description | |------|-------------| +| [application\_id](#output\_application\_id) | Application (client) ID of the Azure AD application for federated credential setup | | [azure\_devops\_organization\_url](#output\_azure\_devops\_organization\_url) | Azure DevOps organization URL | -| [federated\_credential\_issuer](#output\_federated\_credential\_issuer) | Issuer URL for workload identity federation | -| [federated\_credential\_subject](#output\_federated\_credential\_subject) | Subject identifier for workload identity federation | | [key\_vault\_id](#output\_key\_vault\_id) | ID of the Key Vault for storing Azure DevOps PAT | | [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault for storing Azure DevOps PAT | | [key\_vault\_uri](#output\_key\_vault\_uri) | URI of the Key Vault for storing Azure DevOps PAT | diff --git a/modules/azuredevops/service-connection-subscription/backplane/main.tf b/modules/azuredevops/service-connection-subscription/backplane/main.tf index 594577f..53e83b0 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/main.tf +++ b/modules/azuredevops/service-connection-subscription/backplane/main.tf @@ -67,12 +67,3 @@ resource "azurerm_role_assignment" "azure_devops_manager" { role_definition_id = azurerm_role_definition.azure_devops_manager.role_definition_resource_id principal_id = azuread_service_principal.azure_devops.object_id } - -resource "azuread_application_federated_identity_credential" "azure_devops" { - application_id = azuread_application.azure_devops.id - display_name = "${var.service_connection_name}-federated-credential" - description = "Federated identity credential for Azure DevOps service connection" - audiences = ["api://AzureADTokenExchange"] - issuer = "https://vstoken.dev.azure.com/${var.azure_devops_organization_id}" - subject = "sc://${var.azure_devops_organization_url}/${var.azure_devops_project_name}/${var.service_connection_name}" -} diff --git a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf index 94bb9a3..2962bee 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf +++ b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf @@ -33,12 +33,7 @@ output "azure_devops_organization_url" { value = var.azure_devops_organization_url } -output "federated_credential_issuer" { - description = "Issuer URL for workload identity federation" - value = "https://vstoken.dev.azure.com/${var.azure_devops_organization_id}" -} - -output "federated_credential_subject" { - description = "Subject identifier for workload identity federation" - value = "sc://${var.azure_devops_organization_url}/${var.azure_devops_project_name}/${var.service_connection_name}" +output "application_id" { + description = "Application (client) ID of the Azure AD application for federated credential setup" + value = azuread_application.azure_devops.id } diff --git a/modules/azuredevops/service-connection-subscription/backplane/variables.tf b/modules/azuredevops/service-connection-subscription/backplane/variables.tf index 0302061..40d6646 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/variables.tf +++ b/modules/azuredevops/service-connection-subscription/backplane/variables.tf @@ -29,18 +29,3 @@ variable "scope" { description = "Azure scope for role definitions (subscription or management group)" type = string } - -variable "azure_devops_organization_id" { - description = "Azure DevOps organization ID (GUID) for workload identity federation" - type = string -} - -variable "azure_devops_project_name" { - description = "Azure DevOps project name for workload identity federation" - type = string -} - -variable "service_connection_name" { - description = "Azure DevOps service connection name for workload identity federation" - type = string -} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md b/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md index 5d604d4..c33073d 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md @@ -17,11 +17,11 @@ This building block connects your Azure DevOps pipelines to Azure subscriptions, | Assign Azure roles to service principal | ✅ | ❌ | | Create service connection | ✅ | ❌ | | Provide service principal credentials | ✅ | ❌ | +| Automatically configure federated credentials | ✅ | ❌ | | Authorize pipelines to use connection | ⚠️ | ⚠️ | | Use service connection in pipelines | ❌ | ✅ | | Deploy Azure resources via pipelines | ❌ | ✅ | | Monitor deployments | ❌ | ✅ | -| Manage federated credentials | ✅ | ❌ | ## 💡 Best Practices @@ -293,9 +293,9 @@ Run manually to verify connectivity and permissions. **Solution**: 1. Contact Platform Team to verify: - Service principal exists and is active - - Federated identity credential is properly configured - - Azure DevOps organization ID matches the issuer -2. Platform Team will investigate and fix the federated credential configuration + - Backplane was deployed successfully + - Azure AD application is configured correctly +2. The federated credential is automatically created - if there are issues, the Platform Team will need to check the Terraform deployment logs ### Cannot deploy to resource group @@ -324,12 +324,13 @@ Run manually to verify connectivity and permissions. ### No Credential Rotation Required! -This service connection uses **Workload Identity Federation (OIDC)**, which means: +This service connection uses **Workload Identity Federation (OIDC)** with **automatic federated credential setup**, which means: ✅ **No secrets to manage** - authentication uses short-lived tokens ✅ **Automatic token rotation** - tokens expire quickly and are refreshed automatically ✅ **Zero maintenance** - no manual credential rotation needed ✅ **Better security** - no long-lived credentials that can leak or be compromised +✅ **Automatic configuration** - federated credentials are created automatically by the Platform Team ### What This Means for You @@ -341,10 +342,12 @@ This service connection uses **Workload Identity Federation (OIDC)**, which mean **The Platform Team manages**: - Service principal configuration -- Federated identity credential setup +- Automated federated identity credential setup - Azure role assignments - Trust relationship between Azure DevOps and Azure AD +The federated credential is automatically created when your service connection is provisioned, using the exact values from Azure DevOps to ensure perfect compatibility. + ## 📚 Related Documentation - [Azure DevOps Service Connections](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints) diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/README.md b/modules/azuredevops/service-connection-subscription/buildingblock/README.md index 1a24b1e..ca165c8 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/README.md @@ -17,11 +17,12 @@ Creates and manages Azure subscription service connections in Azure DevOps proje - Azure subscription ID to connect to - Azure DevOps PAT stored in Key Vault with `Service Connections (Read, Query & Manage)` scope - Existing Azure AD service principal with appropriate permissions on the target subscription -- Service principal with federated identity credential configured for Azure DevOps +- Application ID from the backplane for federated credential setup ## Features - Configures Azure DevOps service connection using workload identity federation (OIDC) +- Automatically creates federated identity credential with actual Azure DevOps values - No client secrets required - uses secure token-based authentication - Optional automatic authorization for all pipelines - Enhanced security through short-lived tokens @@ -44,26 +45,31 @@ module "azuredevops_service_connection" { azure_subscription_id = "87654321-4321-4321-4321-210987654321" service_principal_id = "11111111-1111-1111-1111-111111111111" azure_tenant_id = "22222222-2222-2222-2222-222222222222" + application_id = azuread_application.azure_devops.id } ``` ### Service Connection with Auto-Authorization ```hcl -module "authorized_service_connection" { +module "backplane" { + source = "./backplane" + # backplane configuration +} + +module "azure_connection" { source = "./buildingblock" - azure_devops_organization_url = "https://dev.azure.com/myorg" - key_vault_name = "kv-azdo-sc-prod" - resource_group_name = "rg-azdo-sc-prod" + azure_devops_organization_url = module.backplane.azure_devops_organization_url + key_vault_name = module.backplane.key_vault_name + resource_group_name = module.backplane.resource_group_name - project_id = "12345678-1234-1234-1234-123456789012" - service_connection_name = "Azure-Dev" + project_id = module.azuredevops_project.project_id + service_connection_name = "Azure-Prod" azure_subscription_id = "87654321-4321-4321-4321-210987654321" - service_principal_id = "11111111-1111-1111-1111-111111111111" - azure_tenant_id = "22222222-2222-2222-2222-222222222222" - authorize_all_pipelines = true - description = "Development environment service connection" + service_principal_id = var.service_principal_id + azure_tenant_id = var.azure_tenant_id + application_id = module.backplane.application_id } ``` @@ -76,10 +82,8 @@ This module exclusively uses **Workload Identity Federation (OIDC)** for enhance The service principal must: 1. Be created and configured outside this module (typically in the backplane) 2. Have appropriate role assignments on the target Azure subscription -3. Have a federated identity credential configured for Azure DevOps with: - - Issuer: `https://vstoken.dev.azure.com/{organization_id}` (GUID, not name) - - Subject: `sc://{org_name}/{project_name}/{connection_name}` - - Audience: `api://AzureADTokenExchange` + +The federated identity credential is automatically created by this module after the service connection is provisioned, using the actual issuer and subject values from Azure DevOps. This ensures perfect alignment between the service connection and the federated credential configuration. ### Benefits @@ -151,6 +155,7 @@ steps: ## Security Considerations - Service principal must be created and managed outside this module +- Federated identity credential is automatically created using actual Azure DevOps values - Workload identity federation uses short-lived tokens (no secrets stored) - Use least privilege principle when assigning roles to the service principal - Enable manual authorization for production service connections @@ -160,8 +165,8 @@ steps: ## Limitations - Service principal must be created and managed separately -- Changing service connection name requires recreation -- Federated identity credential must be configured in the service principal (typically via backplane) +- Changing service connection name requires recreation of both the connection and federated credential +- Federated identity credential is automatically managed by this module - Only workload identity federation is supported (no client secret authentication) @@ -170,6 +175,7 @@ steps: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | +| [azuread](#requirement\_azuread) | ~> 3.6.0 | | [azuredevops](#requirement\_azuredevops) | ~> 1.1.1 | | [azurerm](#requirement\_azurerm) | ~> 4.51.0 | @@ -181,6 +187,7 @@ No modules. | Name | Type | |------|------| +| [azuread_application_federated_identity_credential.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | | [azuredevops_resource_authorization.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/resource_authorization) | resource | | [azuredevops_serviceendpoint_azurerm.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/serviceendpoint_azurerm) | resource | | [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault) | data source | @@ -191,6 +198,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [application\_id](#input\_application\_id) | Azure AD Application ID for federated identity credential (from backplane) | `string` | n/a | yes | | [authorize\_all\_pipelines](#input\_authorize\_all\_pipelines) | Automatically authorize all pipelines to use this service connection | `bool` | `false` | no | | [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | | [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure Subscription ID to connect to | `string` | n/a | yes | diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf index 22eb7e7..ceaf682 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf @@ -41,3 +41,12 @@ resource "azuredevops_resource_authorization" "main" { authorized = true type = "endpoint" } + +resource "azuread_application_federated_identity_credential" "azure_devops" { + application_id = var.application_id + display_name = "${var.service_connection_name}-federated-credential" + description = "Federated identity credential for Azure DevOps service connection" + audiences = ["api://AzureADTokenExchange"] + issuer = azuredevops_serviceendpoint_azurerm.main.workload_identity_federation_issuer + subject = azuredevops_serviceendpoint_azurerm.main.workload_identity_federation_subject +} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf index 0f0bcd6..ec93065 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf @@ -55,3 +55,8 @@ variable "authorize_all_pipelines" { type = bool default = false } + +variable "application_id" { + description = "Azure AD Application ID for federated identity credential" + type = string +} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf b/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf index 6201b42..6e1d052 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf @@ -10,5 +10,9 @@ terraform { source = "microsoft/azuredevops" version = "~> 1.1.1" } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.6.0" + } } } From 43af73b8c54a6163be5d8b68705728b3e2a33049 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 4 Nov 2025 15:08:41 +0100 Subject: [PATCH 2/5] style: formatted --- .../service-connection-subscription/backplane/README.md | 1 - .../service-connection-subscription/buildingblock/README.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/azuredevops/service-connection-subscription/backplane/README.md b/modules/azuredevops/service-connection-subscription/backplane/README.md index c6a85d7..5b687de 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/README.md +++ b/modules/azuredevops/service-connection-subscription/backplane/README.md @@ -79,7 +79,6 @@ No modules. | Name | Type | |------|------| | [azuread_application.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | -| [azuread_application_federated_identity_credential.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | | [azuread_service_principal.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | | [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | | [azurerm_resource_group.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/README.md b/modules/azuredevops/service-connection-subscription/buildingblock/README.md index ca165c8..9633b4f 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/README.md @@ -198,7 +198,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [application\_id](#input\_application\_id) | Azure AD Application ID for federated identity credential (from backplane) | `string` | n/a | yes | +| [application\_id](#input\_application\_id) | Azure AD Application ID for federated identity credential | `string` | n/a | yes | | [authorize\_all\_pipelines](#input\_authorize\_all\_pipelines) | Automatically authorize all pipelines to use this service connection | `bool` | `false` | no | | [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | | [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure Subscription ID to connect to | `string` | n/a | yes | From c6efc352a1e93b42e67f684252ef21f8c4922430 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 4 Nov 2025 15:28:09 +0100 Subject: [PATCH 3/5] fix: fixed application ID parameter to work better with Building blocks --- .../service-connection-subscription/backplane/outputs.tf | 4 ++-- .../service-connection-subscription/buildingblock/README.md | 3 ++- .../service-connection-subscription/buildingblock/main.tf | 2 +- .../buildingblock/service-connection-subscription.tftest.hcl | 2 +- .../buildingblock/variables.tf | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf index 2962bee..9c6b6e4 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf +++ b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf @@ -34,6 +34,6 @@ output "azure_devops_organization_url" { } output "application_id" { - description = "Application (client) ID of the Azure AD application for federated credential setup" - value = azuread_application.azure_devops.id + description = "Application (client) ID of the Azure AD application" + value = azuread_application.azure_devops.client_id } diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/README.md b/modules/azuredevops/service-connection-subscription/buildingblock/README.md index 9633b4f..1519046 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/README.md @@ -45,7 +45,7 @@ module "azuredevops_service_connection" { azure_subscription_id = "87654321-4321-4321-4321-210987654321" service_principal_id = "11111111-1111-1111-1111-111111111111" azure_tenant_id = "22222222-2222-2222-2222-222222222222" - application_id = azuread_application.azure_devops.id + application_id = azuread_application.azure_devops.client_id } ``` @@ -128,6 +128,7 @@ module "azure_connection" { azure_subscription_id = "87654321-4321-4321-4321-210987654321" service_principal_id = var.service_principal_id azure_tenant_id = var.azure_tenant_id + application_id = module.backplane.application_id } ``` diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf index ceaf682..f3d7559 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf @@ -43,7 +43,7 @@ resource "azuredevops_resource_authorization" "main" { } resource "azuread_application_federated_identity_credential" "azure_devops" { - application_id = var.application_id + application_id = "/applications/${var.application_id}" display_name = "${var.service_connection_name}-federated-credential" description = "Federated identity credential for Azure DevOps service connection" audiences = ["api://AzureADTokenExchange"] diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl b/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl index 19e7f8a..22e765d 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl +++ b/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl @@ -4,11 +4,11 @@ variables { resource_group_name = "rg-devops" pat_secret_name = "ado-pat" project_id = "eece6ccc-c821-46a1-9214-80df6da9e13f" - repository_id = "e5612cf3-36f1-4db5-b9d4-6431704233f3" service_connection_name = "test-service-connection" azure_subscription_id = "f808fff2-adda-415a-9b77-2833c041aacf" service_principal_id = "53cc4637-18e2-44f6-8721-dfc08c030dde" + application_id = "53cc4637-18e2-44f6-8721-dfc08c030dde" azure_tenant_id = "5f0e994b-6436-4f58-be96-4dc7bebff827" } diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf index ec93065..ca8b2af 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf @@ -57,6 +57,6 @@ variable "authorize_all_pipelines" { } variable "application_id" { - description = "Azure AD Application ID for federated identity credential" + description = "Azure AD Application client ID (GUID) for federated identity credential" type = string } From f8b3880dff673d18596004d0bdca66e64da63022 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 4 Nov 2025 15:43:24 +0100 Subject: [PATCH 4/5] fix: not application ID but object ID is required --- .../backplane/outputs.tf | 6 +++--- .../buildingblock/README.md | 18 ++++++++++++++---- .../buildingblock/main.tf | 2 +- .../service-connection-subscription.tftest.hcl | 2 +- .../buildingblock/variables.tf | 4 ++-- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf index 9c6b6e4..868ac09 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf +++ b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf @@ -33,7 +33,7 @@ output "azure_devops_organization_url" { value = var.azure_devops_organization_url } -output "application_id" { - description = "Application (client) ID of the Azure AD application" - value = azuread_application.azure_devops.client_id +output "application_object_id" { + description = "Application Object ID (not client ID) of the Azure AD application for federated identity credential setup" + value = azuread_application.azure_devops.object_id } diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/README.md b/modules/azuredevops/service-connection-subscription/buildingblock/README.md index 1519046..36105a1 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/README.md @@ -17,7 +17,17 @@ Creates and manages Azure subscription service connections in Azure DevOps proje - Azure subscription ID to connect to - Azure DevOps PAT stored in Key Vault with `Service Connections (Read, Query & Manage)` scope - Existing Azure AD service principal with appropriate permissions on the target subscription -- Application ID from the backplane for federated credential setup +- **Application Object ID** (not client ID) from the backplane for federated credential setup + +## Important: Application Object ID vs Client ID + +⚠️ **Critical**: The `application_object_id` variable requires the **Application Object ID**, not the Client ID (Application ID). + +- ✅ **Use**: `azuread_application.*.object_id` - Returns the Object ID (GUID) +- ❌ **Don't use**: `azuread_application.*.client_id` - Returns the Client ID (wrong ID) +- ❌ **Don't use**: `azuread_application.*.id` - Returns `/applications/{object_id}` format (will be double-formatted) + +The module transforms the Object ID to `/applications/{object_id}` format required by the federated identity credential resource. ## Features @@ -45,7 +55,7 @@ module "azuredevops_service_connection" { azure_subscription_id = "87654321-4321-4321-4321-210987654321" service_principal_id = "11111111-1111-1111-1111-111111111111" azure_tenant_id = "22222222-2222-2222-2222-222222222222" - application_id = azuread_application.azure_devops.client_id + application_object_id = azuread_application.azure_devops.object_id } ``` @@ -69,7 +79,7 @@ module "azure_connection" { azure_subscription_id = "87654321-4321-4321-4321-210987654321" service_principal_id = var.service_principal_id azure_tenant_id = var.azure_tenant_id - application_id = module.backplane.application_id + application_object_id = module.backplane.application_object_id } ``` @@ -128,7 +138,7 @@ module "azure_connection" { azure_subscription_id = "87654321-4321-4321-4321-210987654321" service_principal_id = var.service_principal_id azure_tenant_id = var.azure_tenant_id - application_id = module.backplane.application_id + application_object_id = module.backplane.application_object_id } ``` diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf index f3d7559..8db2639 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf @@ -43,7 +43,7 @@ resource "azuredevops_resource_authorization" "main" { } resource "azuread_application_federated_identity_credential" "azure_devops" { - application_id = "/applications/${var.application_id}" + application_id = "/applications/${var.application_object_id}" display_name = "${var.service_connection_name}-federated-credential" description = "Federated identity credential for Azure DevOps service connection" audiences = ["api://AzureADTokenExchange"] diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl b/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl index 22e765d..bf349b3 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl +++ b/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl @@ -8,7 +8,7 @@ variables { service_connection_name = "test-service-connection" azure_subscription_id = "f808fff2-adda-415a-9b77-2833c041aacf" service_principal_id = "53cc4637-18e2-44f6-8721-dfc08c030dde" - application_id = "53cc4637-18e2-44f6-8721-dfc08c030dde" + application_object_id = "53cc4637-18e2-44f6-8721-dfc08c030dde" azure_tenant_id = "5f0e994b-6436-4f58-be96-4dc7bebff827" } diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf index ca8b2af..27f7211 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf @@ -56,7 +56,7 @@ variable "authorize_all_pipelines" { default = false } -variable "application_id" { - description = "Azure AD Application client ID (GUID) for federated identity credential" +variable "application_object_id" { + description = "Azure AD Application Object ID (not client ID) - use azuread_application.*.object_id" type = string } From 59f94dcbc1caebd70ae225f11d6a15d3a599429b Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Tue, 4 Nov 2025 15:44:14 +0100 Subject: [PATCH 5/5] style: formatted --- .../service-connection-subscription/backplane/README.md | 2 +- .../service-connection-subscription/buildingblock/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/azuredevops/service-connection-subscription/backplane/README.md b/modules/azuredevops/service-connection-subscription/backplane/README.md index 5b687de..0397472 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/README.md +++ b/modules/azuredevops/service-connection-subscription/backplane/README.md @@ -102,7 +102,7 @@ No modules. | Name | Description | |------|-------------| -| [application\_id](#output\_application\_id) | Application (client) ID of the Azure AD application for federated credential setup | +| [application\_object\_id](#output\_application\_object\_id) | Application Object ID (not client ID) of the Azure AD application for federated identity credential setup | | [azure\_devops\_organization\_url](#output\_azure\_devops\_organization\_url) | Azure DevOps organization URL | | [key\_vault\_id](#output\_key\_vault\_id) | ID of the Key Vault for storing Azure DevOps PAT | | [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault for storing Azure DevOps PAT | diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/README.md b/modules/azuredevops/service-connection-subscription/buildingblock/README.md index 36105a1..1a14b38 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/README.md @@ -209,7 +209,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [application\_id](#input\_application\_id) | Azure AD Application ID for federated identity credential | `string` | n/a | yes | +| [application\_object\_id](#input\_application\_object\_id) | Azure AD Application Object ID (not client ID) - use azuread\_application.*.object\_id | `string` | n/a | yes | | [authorize\_all\_pipelines](#input\_authorize\_all\_pipelines) | Automatically authorize all pipelines to use this service connection | `bool` | `false` | no | | [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | | [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure Subscription ID to connect to | `string` | n/a | yes |