Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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" {
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -97,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 |
Expand All @@ -110,23 +91,19 @@ No modules.

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_azure_devops_organization_id"></a> [azure\_devops\_organization\_id](#input\_azure\_devops\_organization\_id) | Azure DevOps organization ID (GUID) for workload identity federation | `string` | n/a | yes |
| <a name="input_azure_devops_organization_url"></a> [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes |
| <a name="input_azure_devops_project_name"></a> [azure\_devops\_project\_name](#input\_azure\_devops\_project\_name) | Azure DevOps project name for workload identity federation | `string` | n/a | yes |
| <a name="input_key_vault_name"></a> [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault to store the Azure DevOps PAT | `string` | n/a | yes |
| <a name="input_location"></a> [location](#input\_location) | Azure region for resources | `string` | `"West Europe"` | no |
| <a name="input_resource_group_name"></a> [resource\_group\_name](#input\_resource\_group\_name) | Resource group name for the Key Vault | `string` | n/a | yes |
| <a name="input_scope"></a> [scope](#input\_scope) | Azure scope for role definitions (subscription or management group) | `string` | n/a | yes |
| <a name="input_service_connection_name"></a> [service\_connection\_name](#input\_service\_connection\_name) | Azure DevOps service connection name for workload identity federation | `string` | n/a | yes |
| <a name="input_service_principal_name"></a> [service\_principal\_name](#input\_service\_principal\_name) | Name for the Azure DevOps service principal | `string` | `"azure-devops-terraform"` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_application_object_id"></a> [application\_object\_id](#output\_application\_object\_id) | Application Object ID (not client ID) of the Azure AD application for federated identity credential setup |
| <a name="output_azure_devops_organization_url"></a> [azure\_devops\_organization\_url](#output\_azure\_devops\_organization\_url) | Azure DevOps organization URL |
| <a name="output_federated_credential_issuer"></a> [federated\_credential\_issuer](#output\_federated\_credential\_issuer) | Issuer URL for workload identity federation |
| <a name="output_federated_credential_subject"></a> [federated\_credential\_subject](#output\_federated\_credential\_subject) | Subject identifier for workload identity federation |
| <a name="output_key_vault_id"></a> [key\_vault\_id](#output\_key\_vault\_id) | ID of the Key Vault for storing Azure DevOps PAT |
| <a name="output_key_vault_name"></a> [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault for storing Azure DevOps PAT |
| <a name="output_key_vault_uri"></a> [key\_vault\_uri](#output\_key\_vault\_uri) | URI of the Key Vault for storing Azure DevOps PAT |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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_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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@ 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 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

- 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
Expand All @@ -44,26 +55,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_object_id = azuread_application.azure_devops.object_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_object_id = module.backplane.application_object_id
}
```

Expand All @@ -76,10 +92,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

Expand Down Expand Up @@ -124,6 +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_object_id = module.backplane.application_object_id
}
```

Expand Down Expand Up @@ -151,6 +166,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
Expand All @@ -160,8 +176,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)

<!-- BEGIN_TF_DOCS -->
Expand All @@ -170,6 +186,7 @@ steps:
| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.0 |
| <a name="requirement_azuread"></a> [azuread](#requirement\_azuread) | ~> 3.6.0 |
| <a name="requirement_azuredevops"></a> [azuredevops](#requirement\_azuredevops) | ~> 1.1.1 |
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | ~> 4.51.0 |

Expand All @@ -181,6 +198,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 |
Expand All @@ -191,6 +209,7 @@ No modules.

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_application_object_id"></a> [application\_object\_id](#input\_application\_object\_id) | Azure AD Application Object ID (not client ID) - use azuread\_application.*.object\_id | `string` | n/a | yes |
| <a name="input_authorize_all_pipelines"></a> [authorize\_all\_pipelines](#input\_authorize\_all\_pipelines) | Automatically authorize all pipelines to use this service connection | `bool` | `false` | no |
| <a name="input_azure_devops_organization_url"></a> [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes |
| <a name="input_azure_subscription_id"></a> [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure Subscription ID to connect to | `string` | n/a | yes |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ resource "azuredevops_resource_authorization" "main" {
authorized = true
type = "endpoint"
}

resource "azuread_application_federated_identity_credential" "azure_devops" {
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"]
issuer = azuredevops_serviceendpoint_azurerm.main.workload_identity_federation_issuer
subject = azuredevops_serviceendpoint_azurerm.main.workload_identity_federation_subject
}
Original file line number Diff line number Diff line change
Expand Up @@ -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_object_id = "53cc4637-18e2-44f6-8721-dfc08c030dde"
azure_tenant_id = "5f0e994b-6436-4f58-be96-4dc7bebff827"
}

Expand Down
Loading
Loading