Post

Workload identity federation for Azure DevOps with Terraform

Workload identity federation is becoming more and more supported in the Azure ecosystem, and there is already a lot of content on how to use it for deploying Azure resources (I briefly mention it here and there in a GitHub Actions context).
Since last month it’s also supported by the Terraform provider for Azure DevOps. As there are less content for this provider, let’s see in this post how to configure it and make it work with a simple pipeline.

Add a service principal user in your organization

The first thing to do is to create an app registration in Entra ID (formerly Azure AD). Just give it a name and keep the other settings as default:
Create and App Registration in the portal This can also be done with tools like the Azure CLI or PowerShell Then in Azure DevOps, in Organization settings, you can add the service principal just like any normal user (don’t forget to add it to at least one project): Add service principal in Azure DevOps For this sample we will use the Project Contributors group but you might need to use another one depending on your needs

Choosing the Basic access level will consume one licence (the first 5 users are free for each organization)

Before doing this you need to connect your Azure DevOps organization with your Entra ID tenant, checkout the docs here if you need.

Create a service connection with federated credentials

Now that the service principal has access to a project in Azure DevOps, we need to federate its identity with Azure Pipelines runners. From your project settings in Azure DevOps, create a new service connection, select Workload Identity federation (manual), give it a name, and clic Next:
Issuer and subject identifier in Azure DevOps

Here you can retrieve the issuer and the subject identifier that you need to set on the service principal. Go back to your app registration in the Azure portal, click on Certificates & secrets, Federated credentials, Add credential, and select the Other issuer scenario. Then you can report the issuer and subject identifier values from Azure DevOps, give a name to the credential, and click on Add: Issuer and subject identifier in Azure portal

Back in the service connection creation in Azure DevOps, you have probably noticed that it’s saved as a draft, so we need to finalize the set-up. We need to provide the following data:

  • An Azure subscription name and id
  • The service principal id (you can use the Application (client) id from the app registration’s blade)
  • The Entra tenant id (also from the same blade in the Azure portal)

Clicking on Verify and save now will get you an error saying that the service principal (the client) doesn’t have authorization to perform the action Microsoft.Resources/subscriptions/read over the scope of your Azure subscription

Before clicking on Verify and save, as we are using an Azure Resource Manager service connection, Azure DevOps checks that the service principal has at least read access to the Azure subscription. This seems unneeded as we don’t want to interact with Azure here (just with Azure DevOps), but as there is no Azure DevOps service connection type, we need to provide an Azure subscription and give access to it.
So to work around this, simply add a role assignment with the Reader role to the service principal on the Azure subscription (documentation here if you need).

Finally, you can click on Verify and save to finalize the service connection set-up, it should be ok now.

Let’s add some code

This is where the fun begins, as the set-up steps are finally done. As a simple example, let’s say we want to data source our Azure DevOps project in Terraform and output its id, so the main.tf looks like this:

1
2
3
4
5
6
7
data "azuredevops_project" "test" {
  name = var.project_name
}

output "project_id" {
  value = data.azuredevops_project.test.id
}

Let’s focus on the provider configuration, as you can see in the provider’s documentation here, we need to provide a tenant id, a client id, an OIDC token 😱, the URL of the Azure DevOps organization, and specify the use_oidc flag.
There are several ways to provide these values: directly in the code, through environment variables, or text files… I have chosen to only set the use_oidc flag in the code to keep it simple:

1
2
3
4
5
6
7
8
9
10
11
12
terraform {
  required_providers {
    azuredevops = {
      source  = "microsoft/azuredevops"
      version = ">=1.0"
    }
  }
}

provider "azuredevops" {
  use_oidc = true
}

Now on the pipeline side, all we need is an AzureCLI task so that we can use the service connection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: TestTerraformAdoProvider

steps:
  - task: AzureCLI@2
    name: RunTerraform
    inputs:
      azureSubscription: sc-azdo-oidc-demo # The name of your service connection goes here
      scriptType: bash
      scriptLocation: inlineScript
      addSpnToEnvironment: true
      inlineScript: |
        export ARM_TENANT_ID="$tenantId"
        export ARM_CLIENT_ID="$servicePrincipalId"
        export ARM_OIDC_TOKEN="$idToken"
        terraform init
        terraform apply -auto-approve
    env:
      AZDO_ORG_SERVICE_URL: $(System.CollectionUri)
      TF_VAR_project_name: $(System.TeamProject)

A few things to notice here:

  • The addSpnToEnvironment input is key: it populates the $tenantId, $servicePrincipalId and $idToken variables in the inline script so we can pass them to Terraform through environment variables
  • Predefined System variables are used to get the Azure DevOps organization URL and project name so that we don’t need to hard-code them 👌

And that’s it, running this pipeline should get a result like this: Pipeline run in Azure DevOps

Which might seem pointless but demonstrates that the federated connection between the Azure Pipelines agents and Azure DevOps works through the Terraform provider 🙌

What about OpenTofu ?

Before closing this post, I realize that I have only considered Terraform users and not OpenTofu’s. I don’t want to comment the licence/legal business here, and keep focused on the technical stuff.

So the HCL code above works perfectly with OpenTofu without any change. For the YAML pipeline, Terraform is already installed on Microsoft hosted agents, but OpenTofu is not (and it’s not planned).

Updating the inlineScript input by adding a command to install OpenTofu and replacing terraform commands by tofu will make it work:

1
2
3
4
5
6
7
      inlineScript: |
        sudo snap install --classic opentofu
        export ARM_TENANT_ID="$tenantId"
        export ARM_CLIENT_ID="$servicePrincipalId"
        export ARM_OIDC_TOKEN="$idToken"
        tofu init
        tofu apply -auto-approve

Of course, you’ll probably need to adapt the snippets shared here to your real-world situation (for instance putting OpenTofu’s installation in a dedicated task).

Wrapping-up

In this post we have seen how to use the federated connection with the Terraform provider for Azure DevOps. I usually don’t do many tutorial posts like this, but as it’s way easier to find content about Azure (not DevOps), and the documentation is split across several websites, I guess providing a full tutorial is worth it.
As I have learned a few things about authentication in Azure DevOps, there might be other posts on this fancy topic in the future 🤓

This post is licensed under CC BY 4.0 by the author.