Automation · CI-CD · Command · docker · Go · Infrastructure As Code · terraform

Terraforming Azure DevOps

Background

In many organizations, specially in large enterprises there’s a need to automate Azure DevOps projects and Teams members. Manually managing large number of Azure DevOps projects, Teams for these projects and users to the teams, on-boarding and off-boarding team members are not trivial.

Besides managing the users sometimes, we just need to have an overview (a documentation?) of users and Teams of Projects. Terraform is a great tool for Infrastructure as Code – which not only allows providing infrastructure on demand, but also gives us nice documentation which can be versioned control in a source control system. The workflow kind of looks like following:

GitOps

I am developing a Terraform Provider for Azure DevOps that helps me use Terraform for provisioning Azure DevOps projects, Teams and members. In this article I will share how I am building it.

Note
This provider doesn't implement the complete set of 
Azure DevOps REST APIs. 
Its limited to only projects, teams and member associations. 
It's not recommended to use it in production scenarios.

Terraform Provider

Terrafom is an amazing tool that lets you define your infrastructure as code. Under the hood it’s an incredibly powerful state machine that makes API requests and marshals resources. Terraform has lots of providers – almost for every major cloud – out there. Including many other systems – like Kubernetes, Palo-Alto Networks etc.

In nutshell if any system has REST API that can be manipulated with Terraform Provider. Azure DevOps also has a terraform provider – which doesn’t currently provide resources to create Teams and members. Hence, I am writing my own – shamelessly using/stealing the Microsoft’s Terraform provider (referenced above) for project creation.

Setting up GO Environment

Terraform Providers and plugins are binaries that Terraform communicates during runtime via RPC. It’s theoretically possible to write a provider in any language, but to be honest, I haven’t come across any providers that were written other languages than GO. Terraform provide helper libraries in Go to aid in writing and testing providers.

I am developing in Windows 10 and didn’t want to install GO on my local machine. Containers come to rescue of course. I am using the “Remote development” extension in VS Code. This extension allows me to keep the source code in local machine and compile, build the source code in a container like Magic!

remote

Figure: Remote Development extension in VSCode – running container to build local repository.

Creating the provider

To create a Terraform provider we need to write the logic for managing the Creation, Reading, Updating and Deletion (CRUD) of a resource (i.e. Azure DevOps project, Team and members in this scenario) and Terraform will take care of the rest; state, locking, templating language and managing the lifecycle of the resources. Here in this repository I have a minimum implementation that supports creating Azure DevOps projects, Teams and its members.

First of all we define our provider and resources in main.go file.


package main
import (
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: func() terraform.ResourceProvider {
return Provider()
},
})
}

view raw

main.go

hosted with ❤ by GitHub

Next to that, we will define the provider schema (the attributes it supports as input and outputs, resources etc.)


package main
import (
"github.com/hashicorp/terraform/helper/schema"
)
// Provider – The top level Azure DevOps Provider definition.
func Provider() *schema.Provider {
p := &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"azuredevops_pipeline": resourcePipeline(),
"azuredevops_project": resourceProject(),
"azuredevops_team": resourceTeam(),
"azuredevops_serviceendpoint": resourceServiceEndpoint(),
},
Schema: map[string]*schema.Schema{
"org_service_url": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("AZDO_ORG_SERVICE_URL", nil),
Description: "The url of the Azure DevOps instance which should be used.",
},
"personal_access_token": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("AZDO_PERSONAL_ACCESS_TOKEN", nil),
Description: "The personal access token which should be used.",
},
},
}
p.ConfigureFunc = providerConfigure(p)
return p
}
func providerConfigure(p *schema.Provider) schema.ConfigureFunc {
return func(d *schema.ResourceData) (interface{}, error) {
client, err := getAzdoClient(d.Get("personal_access_token").(string), d.Get("org_service_url").(string))
return client, err
}
}

view raw

provider.go

hosted with ❤ by GitHub

We are using Azure DevOps personal Access token to communicate to the Azure DevOps REST API. The GO client for Azure DevOps from Microsoft – which is used as dependency, immensely simplified the implementation and also helped learning the flow.

Now defining the “team” resource as following:


package main
import (
"fmt"
"github.com/microsoft/terraform-provider-azuredevops/utils/converter"
"github.com/microsoft/terraform-provider-azuredevops/utils/tfhelper"
"os"
"log"
"github.com/google/uuid"
"github.com/hashicorp/terraform/helper/schema"
"github.com/microsoft/azure-devops-go-api/azuredevops/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/graph"
)
func resourceTeam() *schema.Resource {
return &schema.Resource{
Create: resourceTeamCreate,
Read: resourceTeamRead,
Update: resourceTeamUpdate,
Delete: resourceTeamDelete,
//https://godoc.org/github.com/hashicorp/terraform/helper/schema#Schema
Schema: map[string]*schema.Schema{
"project_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
DiffSuppressFunc: tfhelper.DiffFuncSupressCaseSensitivity,
},
"name": &schema.Schema{
Type: schema.TypeString,
ForceNew: true,
Required: true,
DiffSuppressFunc: tfhelper.DiffFuncSupressCaseSensitivity,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"members": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"admin": {
Type: schema.TypeBool,
Required: true,
Sensitive: true,
},
},
},
},
},
}
}

That’s all for declaring, now implementing the CRUD methods in resource providers. The full source code is in GitHub.

We can compile the provider application using following command:

> GOOS=windows GOARCH=amd64 go build -o terraform-provider-azuredevops.exe

As I am using Dabian docker image for GoLang I need to specify my target OS (GOOS=windows) and CPU Architecture (GOARCH=amd64) when I build the provider. This will produce the terraform provider for Azure DevOps executable.

Although it’s executable, it’s not meant to launch directly from command prompt. Instead, I will copy it to “%APPDATA%\ terraform.d\plugins\windows_amd64” folder of my machine.

Terraform Script for Azrue DevOps

Now we can write the Terraform file (.tf) that will describe the Azure DevOps Project, Team and members etc.

Terraform

With this terraform file, we can now launch the following command to initialize our terraform environment.

init

The terraform init command is used to initialize a working directory containing Terraform configuration files. This is the first command that should be run after writing a new Terraform configuration or cloning an existing one from version control.

Terraform plan

The terraform plan command is used to create an execution plan. Terraform performs a refresh, and then determines what actions are necessary to achieve the desired state specified in the configuration files. This command is a convenient way to check whether the execution plan for a set of changes matches your expectations without making any changes to real resources or to the state. For example, terraform plan might be run before committing a change to version control, to create confidence that it will behave as expected.

PLan

Figure: terraform plan output – shows exactly what is going to happen if we apply these changes to Azure DevOps

Terraform apply

The terraform apply command is used to apply the changes required to reach the desired state of the configuration, or the pre-determined set of actions generated by a terraform plan execution plan. We will launch it with an “-auto-approve” flag to assert the approval prompt.

apply

Now we can go to our Azure DevOps and sure enough there’s a new project created with the configuration as we scripted in Terraform file.

Taking it further

Now we can check in the terraform file (main.tf above) into an Azure DevOps repository and put a Branch policy to it. That will force any changes (such as creating new projects, adding removing team members) would requrie a Pull-Request and needs to be reviewed by peers (four-eyes principles). Once Pull-Requests are approved, a simple Azure Pipeline can trigger that does the terraform apply. And I have my workflow automated  and I also have nice histories in GIT – which records the purpose of any changes made in past.

Thanks for reading!

One thought on “Terraforming Azure DevOps

  1. Interresting – I also started to develop and publish a Terraform provider for Azure DevOps

    https://github.com/mikaelkrief/terraform-provider-azuredevops

    At the time I started ; the Go client didn’t exist and I had to generate it myself : https://github.com/mikaelkrief/go-azuredevops-sdk

    And the doc is here https://app.gitbook.com/@azure-devops-terraform-provider/s/project/

    I will be very interrested to contribute with on your provider

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s