Azure DevOps uses service connections to access services that are targets for cloud infrastructure provisioning and application deployment. The most commonly used service connection is the Azure Resource Manager service connection. By default, this creates an object in Azure DevOps, an identity in Entra ID and a role assignment in Azure.
Many Enterprise customers have requirements around the management of Entra workload identities (applications, service principals, managed identities) as well as the permissions identities are granted.
Here are a few common requirements and constraints:
- Creation of app registrations is restricted in the Entra ID tenant and/or the use of Managed Identities for Azure access is mandated
- Specific secret expiration and auto-rotation control
- ITSM metadata is required on Entra ID objects (service management reference, naming convention, notes)
- Co-owners are required to exist on Entra ID apps
- Access is managed through Entra ID group membership, access control lists should not include individual principals
- Custom role assignments are needed for Azure data plane access e.g. Key Vault, Kusto, Storage
- Access needs to be granted to multiple Azure subscriptions that are not part of the same management group
- An IT fulfillment process exists where identities are automatically provisioned based on a service request
Terraform employs a plugable provider model which enables all changes to be made by a single tool and configuration:
Service | Provider | API |
---|---|---|
Azure | azurerm | Azure Resource Manager REST API |
Azure DevOps | azuredevops | Azure DevOps REST API |
Entra ID | azuread | Microsoft Graph API |
HCL, the language used, is declarative and the tool is capable if inferring dependencies to create resources in order. For the most part, dependencies do not have to be declared explicitly. This is the output generated by terraform graph
:
More information:
- Overview of Terraform on Azure - What is Terraform?
- Cloud Adoption Framework - Infrastructure-as-Code CI/CD security guidance
Provisioning is a matter of specifying Terraform variables (see inputs below) and running terraform apply
. To set variables, you can create a .auto.tfvars file, see sample.
To understand how the Terraform configuration can be created in automation, review
deploy.ps1 and the
CI pipeline.
Below are common configurations. You can mix & match these to meet specific requirements.
This creates an App registration with Federated Identity Credential and Contributor
role on the Azure subscription used by the Terraform azurerm
provider.
azdo_organization_url = "https://dev.azure.com/my-organization"
azdo_project_name = "my-project"
Pre-requisites:
- The user can create app registrations i.e.:
- Creation of app registrations is not disabled in Entra ID; or
- The user is member of a privileged Entra ID role e.g. Application Developer
- The user is an owner of the Azure subscription (so role assignment can be performed)
This creates an Entra ID app registration with IT service reference and notes fields populated as well as specifying co-owners:
azdo_organization_url = "https://dev.azure.com/my-organization"
azdo_project_name = "my-project"
create_managed_identity = false
credential_type = "FederatedIdentity"
entra_app_notes = "Service connection for business application ABC deployment to XYZ environment"
entra_app_owner_object_ids = ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111"]
entra_service_management_reference = "11111111-1111-1111-1111-111111111111"
Pre-requisites:
- The user can create app registrations i.e.:
- Creation of app registrations is not disabled in Entra ID; or
- The user is member of a privileged Entra ID role e.g. Application Developer
- The user is an owner of the Azure subscription (so role assignment can be performed)
This creates an Entra ID app registration with secret that expires after 1 hour:
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_managed_identity = false
credential_type = "Secret"
entra_secret_expiration_days = 0 # secret lasts 1 hour
Pre-requisites:
- The user can create app registrations i.e.:
- Creation of app registrations is not disabled in Entra ID; or
- The user is member of a privileged Entra ID role e.g. Application Developer
- The user is an owner of the Azure resource group (so role assignment can be performed)
This creates a Managed Identity with Federated Identity Credential and custom Azure RBAC (role-based access control) role assignments:
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 = "Storage Blob Data Contributor"
},
{
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
role = "Key Vault Secrets User"
}
]
credential_type = "FederatedIdentity"
create_managed_identity = true
managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/msi-rg"
Pre-requisites:
- A resource group to hold the Managed Identity has been pre-created
- The user is an owner of the Azure scopes to create role assignments on
This creates a Managed Identity with Federated Identity Credential and custom Azure RBAC (role-based access control) role assignments:
azdo_organization_url = "https://dev.azure.com/my-organization"
azdo_project_name = "my-project"
azdo_service_connection_type = "ACR"
azure_container_registry_name = "myregistry"
azure_role_assignments = [
{
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.ContainerRegistry/registries/myregistry"
role = "AcrPush"
}
]
credential_type = "FederatedIdentity"
create_managed_identity = true
managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/msi-rg"
Pre-requisites:
- A resource group to hold the Managed Identity has been pre-created
- The user is an owner of the Azure scopes to create role assignments on
This creates a Managed Identity with Federated Identity Credential and custom Azure RBAC (role-based access control) role assignments:
azdo_organization_url = "https://dev.azure.com/my-organization"
azdo_project_name = "my-project"
azure_role_assignments = [] # No direct assignments
create_managed_identity = true
credential_type = "FederatedIdentity"
entra_security_group_names = ["my-security-group"]
managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/msi-rg"
Pre-requisites:
- A resource group to hold the Managed Identity has been pre-created
- The user is an owner of the Entra ID security group to add the Managed Identity to
The (required) variables and output are listed below. Sensitive outputs are masked by default. Generated with terraform-docs.
Name | Version |
---|---|
azuread | 2.53.1 |
azurerm | 4.6.0 |
external | 2.3.4 |
random | 3.6.3 |
terraform | n/a |
Name | Source | Version |
---|---|---|
acr_service_connection | ./modules/azure-devops-acr-service-connection | n/a |
azure_role_assignments | ./modules/azure-access | n/a |
azure_service_connection | ./modules/azure-devops-azure-service-connection | n/a |
entra_app | ./modules/entra-application | n/a |
managed_identity | ./modules/azure-managed-identity | n/a |
Name | Description | Type | Default | Required |
---|---|---|---|---|
azdo_organization_url | The Azure DevOps organization URL (e.g. https://dev.azure.com/contoso) | string |
n/a | yes |
azdo_project_name | The Azure DevOps project name to create the service connection in | string |
n/a | yes |
azdo_creates_identity | Let Azure DevOps create identity for service connection | bool |
false |
no |
azdo_service_connection_type | The type of service connection to create. Valid values are 'Azure' and 'ACR'. | string |
"Azure" |
no |
azure_container_registry_name | The Azure Container Registry name | string |
null |
no |
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})) |
null |
no |
create_managed_identity | Creates a Managed Identity instead of a App Registration | bool |
false |
no |
credential_type | The type of credential to use for the service connection. Valid values are 'FederatedIdentity' and 'Secret'. | string |
"FederatedIdentity" |
no |
entra_app_notes | Description to put in the Entra ID app registration notes field | string |
null |
no |
entra_app_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 | Secret expiration in days | number |
90 |
no |
entra_security_group_names | Names of the security groups to add the service connection identity to | list(string) |
[] |
no |
entra_service_management_reference | IT Service Management Reference to add to the App Registration | string |
null |
no |
managed_identity_resource_group_id | The resource group to create the Managed Identity in | string |
null |
no |
resource_prefix | The prefix to put in front of resource names created | string |
"demo" |
no |
resource_suffix | The suffix to append to resource names created | string |
"" |
no |
run_id | The ID that identifies the pipeline / workflow that invoked Terraform (used in CI/CD) | number |
null |
no |
Name | Description |
---|---|
azdo_project_id | The Azure DevOps project id the service connection was created in |
azdo_service_connection_id | The Azure DevOps service connection id |
azdo_service_connection_name | The Azure DevOps service connection name |
azdo_service_connection_url | The Azure DevOps service connection portal URL |
azure_role_assignments | Role assignments created for the service connection's identity |
azure_subscription_id | The Azure subscription id the service connection was granted access to |
azure_subscription_name | The Azure subscription name the service connection was granted access to |
entra_app_notes | Description provided in the app registration notes field |
identity_application_id | The app/client id of the service connection's identity |
identity_application_name | The name of the service connection's identity |
identity_federation_subject | The federation subject |
identity_issuer | The federation issuer |
identity_object_id | The object id of the service connection's identity |
identity_principal_id | The service principal id of the service connection's identity |
identity_principal_name | The service principal name of the service connection's identity |
identity_principal_url | The service principal portal url of the service connection's identity |
identity_secret | The secret of the service connection's identity |
identity_secret_end_date | The secret expiration date of the service connection's identity |
identity_url | The portal url of the service connection's identity |