Terraforming Azure DevOps Services Terraforming Azure DevOps Services

Terraforming Azure DevOps Services

Microsoft has created a Terraform provider for Azure DevOps and published it to the Terraform Registry. At the time of writing this article, the provider version is 0.2.1 and does not include support for Marketplace Extension management.

Terraform enables us to use null_resource and local-exec provider to execute PowerShell scripts. We can parametrise those scripts and pass them to Terraform variables in execution time. Once we’re in PowerShell, all kinds of new possibilities emerge. Essentially, any resource that does not (yet) exist in a particular Terraform provider but is controllable from third party CLI, PowerShell Module, or exposed API, can be utilized this way.

Here, we’ll describe how to use PowerShell and Azure DevOps REST API to fill this gap and install multiple extensions within our Terraform solution.

Terraforming Azure DevOps Services diagram

In our example, we’ll use Powershell as a local executor to run the script while passing Terraform variables as parameters. These parameters will be used to authenticate with Azure DevOps and execute REST API requests to install multiple extensions to our Azure DevOps organization.

Prerequisites

In order to establish connectivity and set necessary role assignments, we need to have:

  • An existing Terraform project
  • An existing Azure DevOps organization
  • Azure DevOps personal access token with extension permissions
  • PowerShell, preferably version 5.0 or above
  • Desired extensions data triplets (extensionId, publisherId, and version)

System Description

The approach is simple:

  1. Declare Terraform null_resource and specify local-exec provisioner. 
  2. Specify path to PowerShell script and pass arguments to the Script from Terraform variables.
  3. Do whatever you need to do within the PowerShell script.


resource "null_resource" "InstallExtensionsScript" {
provisioner "local-exec" {
command = ".'${path.module}/InstallExtension.ps1' -pat ${var.ado_pat_extensions} -orgName ${var.ado_org_name}"
interpreter = ["PowerShell", "-Command"]
}
}

We are using variables:

  • {ado_pat_extensions} – Personal access token with extensions permissions, placed within variables.tf
  • {ado_org_name} – Name of your Azure DevOps organization, also placed within your variables.tf file

The exact procedure to create PAT is described here. It is good practice not to use Full Access PATs so for this case, permissions needed for extensions management are shown in the following figure:

image (51)

PowerShell Script

For the execution of this script, we’ll use descriptors of the extensions that can be extracted from the marketplace link and homepage of the extension. We could save this configuration of the extensions in a separate file, but for the purpose of this article, this will be placed within the PowerShell script itself. Specifically, we need the extensionId, publisherId, and version of each extension we are installing. For instance, if you use the Create Work Item extension, here is the marketplace link.

Notice the string at the end, “mspremier.CreateWorkItem,” as it exposes much needed publisherId and extensionId, mandatory parameters for our unattended installation. Version can be found at the page itself:

 

More info

We can see that “Microsoft” is stated as Publisher here, but this is misleading and the publisherId that works with our REST API calls is stated within the URL of the Marketplace Extension page, which in this case is “mspremier.” So, keep your eyes on that one!

If for some reason this approach doesn’t work, take a look at the end of this article for another way to discover this information wrapped in a small script for your convenience.

Back to our extensions specification, we will use it in our script like this:


class Extension {
[string] $extensionId
[string] $publsherId
[string] $version
}
$extensionSet = @(
[Extension]@{extensionId='relacetokens';publisherId="qetza"; version="4.4.0"}
[Extension]@{extensionId='CreateWorkItem';publisherId="mspremier";version="1.17.0"}
)

Within our script, we will create authentication header using Personal Access Token like this:


$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($pat)"))
$header = @{authorization = "Basic $token"}

 

Azure DevOps services uses special Base URL for REST API Extensions management (dont try to send requests to your Organization URL, but this “extmgmt” one):

$orgUrl = "https://extmgmt.dev.azure.com/$orgName"

Using REST API we will first check for the installed extensions like this:


$installedExtensionsUrl = "$orgUrl/_apis/extensionmanagement/installedextensions"+'?'+$queryString
$extensions = Invoke-RestMethod -Uri $installedExtensionsUrl -Method Get -ContentType "application/json" -Headers $header
$result = $extensions.value.Where({$_.extensionId -eq $extensionMember.extensionId})

If there are no results, we can proceed with installation of the extension like this:


$installExtensionUrl = "$orgUrl/_apis/extensionmanagement/installedextensionsbyname/"+$extensionMember.publisherId + "/" + $extensionMember.extensionId+ "/" + $extensionMember.version + '?' + $queryString< $extension = Invoke-RestMethod -Uri $installExtensionUrl -Method Post -Headers $header -ContentType "application/json" $extension Write-Host "INFO: Extension " $extensionMember.extensionId" installed."

The script actually tries to install each of the specified extensions in a foreach loop and informs the host of the outcome.

Additional resources about Azure DevOps Extensions REST API can be found here:


You can find InstallExtensions script at this repo: InstallExtensions.ps1

I’ve mentioned earlier that for some extensions it might be difficult to find extensionId and publisherId. The “hard” way to find this out would be to perform manual installation of the Extension and execute following script to find out those parameters:

ExtensionParametersDiscovery.ps1

The script will ask you for the PAT, Organization name and Extension name and once executed, will display that precious info like this:

Blue code

This information will enable you to install the specific Extension using Terraform and PowerShell script approach the next time and will ensure you have it within IaC specification.

In Summary

This Terraform – PowerShell blend can help you achieve a higher level of IaC automation. However, it is my assumption that over time, published Terraform providers will grow in their capabilities and will enable plain Terraforming of Azure DevOps extensions. When that happens, the need for this specific PowerShell script will and should diminish.

Until then, we’re free to use the described approach, which enables us to create entire ecosystems with a single Terraform apply command.

*The script used as an example within this article is not connected to any Newfire customer projects.