Administration of Azure DevOps on a large scale in minutes.

Tech Community • 10 min read

Azure DevOps Service is a cloud native Software as a Service (SaaS) from Microsoft that provides organizations with a fully integrated platform for software development. The focus is on built-in functions such as

to enable a culture of collaboration that eliminates the need for additional tools and integration overhead with third-party systems. Configuration can be performed either via a user interface (web browser) or a REST-API by the authorized personnel. The challenge for administrators, in particular, is to implement repeating activities efficiently, in an automated manner, and with a high degree of quality. Nobody wants to waste their time with unnecessary activities!

So in the further part of the article you will not find screenshots of user interfaces in the browser, but the focus is on the programmatic implementation.

Terraform

Terraform is an amazing infrastructure as Code tool that lets you build, change and version resources (e.g. cloud or on-premise) safely and efficiently. In this section you will not find a deep dive on how to use Terraform (basic knowledge is required). You can find a starting point for learning at Hashicorp.

I just want to address one fundamental concept of Terraform - the Provider.

"Providers are a logical abstraction of an upstream API. They are responsible for understanding API interactions and exposing resources." (Hashicorp definition)

The use of providers significantly extends the functionality of Terraform (analogy e.g. to plug-ins in other programming languages or frameworks). 

The best-known examples include AWS, Azure and Google Cloud Platform for the management of public cloud infrastructure (e.g. virtual machines, networks, traffic routing, databases, identity & access management, etc.).

In addition to the popular use cases, there are other providers, e.g. for interacting with resources (e.g. Kubernetes) or applications or platforms (e.g. Github, Azure Active Directory). Microsoft also provides a Terraform Provider for Azure DevOps for administrative purposes, which I will introduce in more detail in the following sections.

At the beginning we register the provider

Unset

terraform {
  required_version = "~> 1.4"

  required_providers {
    azuredevops = {
	source  = "microsoft/azuredevops"
	version = "~> 0.5"
    }
  }
}

Then we create a PAT and initialize the provider

Unset

provider "azuredevops" {
  org_service_url       = "https://dev.azure.com/<your organisation name>"
  personal_access_token = "<your personal access token>"
}

Please replace the placeholders <your organization name> and <your personal access token> with your values.

Azure DevOps

Project

A project establishes an organizational umbrella for collaboration (e.g. management and progress) in the development of software solutions (e.g. source code). In practice, projects are used to isolate functional teams, for example cross-functional teams in agile organizations, from each other to enable independent lifecycles.

From an administrator's perspective, managing projects simply means using a provider resource

Unset

resource "azuredevops_project" "project" {
  name               = var.name
  description        = var.description
  features           = var.features
  visibility         = var.visibility
  work_item_template = var.template
}

which defines useful default values or values that vary from them depending on requirements. In this example, an agile project is enabled with all features except Azure Testplans. (no additional administrative effort required).

Unset

variable "features" {
  type        = map(string)
  description = "Defines the status (enabled, disabled) of the all project features."
  default     = {
    boards       = "enabled"
    repositories = "enabled"
    pipelines    = "enabled"
    artifacts    = "enabled"
    testplans    = "disabled" # disabled by default because of additional license costs
  }
}

variable "template" {
  type        = string
  description = "Specifies the work item template. Valid values: Agile, Basic, CMMI, Scrum."
  default     = "Agile"
}

Security

In Azure DevOps projects, it is important to define which identities are granted access to resources (RBAC) in order to fulfill their responsibilities (e.g., developer or operator). The baseline for this is usually an existing authorization concept in the organization, which has been implemented by groups and memberships of people in Azure Active Directory. Based on this implementation, it is very easy to apply Azure AD groups to Azure DevOps built-in roles.

Unset

data "azuredevops_group" "project" {
  for_each = var.project

  name       = each.key
  project_id = var.project_id
}

resource "azuredevops_project_permissions" "project" {
  for_each = var.project

  project_id  = var.project_id
  principal   = data.azuredevops_group.project[each.key].id
  permissions = each.value.permissions
}

A suitable configuration is very easy to define, as it consists of a mapping from Built-in role names to Azure AD Group names.

Unset

# Use Azure AD groups to manage built-in group permissions
rbac = {
  Readers      = ["project-guests"]
  Contributors = ["project-developers", "project-supporters"]
}

Another nice functionality is to assign permissions to Azure AD groups at the GIT repository level to map finely granular permissions for the development process.

Unset

data "azuredevops_group" "git" {
  for_each = var.git

  name       = each.key
  project_id = var.project_id
}

resource "azuredevops_git_permissions" "git" {
  for_each = var.git

  project_id  = var.project_id
  principal   = data.azuredevops_group.git[each.key].id
  permissions = each.value.permissions
}

In this simple example, explicit permissions are set for a GIT repository for the Built-in role Contributor. For a complete list of permissions, see the provider resource.

Unset

Contributors = {
  permissions = {
    ForcePush        = "Allow"
    CreateRepository = "Allow"
    DeleteRepository = "Allow"
    RenameRepository = "Allow"
  }
}

Repositories

Within an Azure DevOps project, a repository contains all types of source code (e.g. business applications or infrastructure as code, etc.) in the form of a version control system.

Unset

resource "azuredevops_git_repository" "repo" {
  name           = var.name
  project_id     = var.project_id
  default_branch = var.default_branch

  initialization {
    init_type = "Clean"
  }
}

Furthermore, files can be added at creation time (e.g. CI/CD pipelines, README.md, or similar) to help development teams adapt standard processes without significant effort (more use cases on this can be found below).

Unset

resource "azuredevops_git_repository_file" "initial" {
  for_each = var.files

  branch              = var.default_branch
  content             = each.value.content
  file                = join("/", [each.value.path, each.key])
  repository_id       = azuredevops_git_repository.repo.id
  commit_message      = "Initial commit."
  overwrite_on_create = true
  lifecycle {
    ignore_changes = [
      content
    ]
  }
}

In the following example configuration, a repository named "demo" is created and a file ".gitignore" from a standard template is added in the root directory (git commit).

Unset

repos = {
  demo = {
    files = {
      ".gitignore" = {
        content = data.template_file.tf_gitignore.rendered
        path    = "/"
      }
    }
  }
}

Service Connections

A Service Connection provides a communication proxy between an Azure DevOps project to a remote service (e.g. Azure Resource Manager or Github etc.). This creates a stable API e.g. for Azure Pipelines, which avoids e.g. explicit storage of credentials. Only one provider resource is required for administration; in addition, the principle of least privilege is implemented in the best possible way because the creation and authorization of the service principal can be designed in a fine-grained manner (assuming privileged permissions in Azure AD).

In this example, a service connection is established to the Azure Resource Manager API. The service principal used can either be created earlier or an existing one can be configured for it. The permissions of this account are essential for the functionality of the connection (authorization process).

Unset

resource "azuredevops_serviceendpoint_azurerm" "arm" {
  project_id            = var.project_id
  service_endpoint_name = var.arm.name
  description           = "Managed by terraform"

  credentials {
    serviceprincipalid  = var.arm.client_id
    serviceprincipalkey = var.arm.client_secret
  }

  azurerm_spn_tenantid          = var.arm.tenant_id
  azurerm_subscription_id       = var.arm.subscription_id
  azurerm_subscription_name     = var.arm.subscription_name
  azurerm_management_group_name = var.arm.resource_group_name
}

Pipelines & Agent Pools

Azure Pipelines and Agent Pools together provide a service for automating repeated tasks, e.g. building, testing and deploying applications or managing infrastructure. All of these activities are integral to DevOps in the classic sense and can also be managed with low effort.
A pipeline resource is created by this snippet

Unset

resource "azuredevops_build_definition" "pipeline" {
  project_id = var.project_id
  name       = var.name
  path       = replace(var.path, "/", "\\")

  ci_trigger {
    use_yaml = true
  }

  repository {
    repo_type   = "TfsGit"
    repo_id     = var.repository_id
    branch_name = var.default_branch
    yml_path    = join("/", [var.path, var.file_name])
  }
}

and specified by this configuration. Here also the content of a template is used to create a pipeline in the root directory “/azdo”. This allows pipelines to be provided flexibly.

Unset

pipelines = {
  "azure-pipelines.yaml" = {
    content = data.template_file.pipeline.rendered
    path    = "/azdo"
  }
}

Agent pools can in general also be managed with Terraform, but the implementation e.g. with Azure Virtual Machine Scale Set agents is not fully programmatically possible. For this case, I recommend a workaround using the Azure DevOps Elasticpools REST API  in conjunction with a Lifecycle Terraform resource to orchestrate the HTTP operations within a shell script as follows.

Unset

resource "shell_script" "pool" {
  lifecycle_commands {
    create = "sh ${path.module}/scripts/pool_http_client.sh create"
    update = "sh ${path.module}/scripts/pool_http_client.sh update"
    delete = "sh ${path.module}/scripts/pool_http_client.sh delete"
  }

  environment = {
    POOL_NAME             = var.name
    PROJECT_ID            = var.project_id
    BASE_URL              = local.base_url
    REQUEST_BODY_JSON     = local.pool_create_body
    SERVICE_ENDPOINT_NAME = var.service_endpoint_name
    VM_SCALE_SET_ID       = var.virtual_machine_scale_set_id
  }
}

Important note: The current implementation in this example uses an existing Azure virtual machine scale set that is to be used as an agent pool (incl. network configuration for inbound traffic). Creating this infrastructure is not (yet) part of the solution, but an architectural decision. The focus is on managing Azure DevOps as a platform and reducing dependencies (e.g. Azure infrastructure incl. necessary permissions to manage Azure resources). It is of course technically possible at this point to implement a connector into your own Azure infrastructure.

Short recap

In previous sections, I presented the elementary building blocks for managing Azure DevOps projects. They are based almost entirely on Microsoft's Azure DevOps Terraform Provider and offer administrators a convenient way to perform their daily work with a minimum of time while maintaining high quality. An effort usually only occurs when, differing from the standard, individual requirements for the configuration of Azure DevOps projects have to be mapped (text change in the Terraform configuration). The solution is very smart, controllable (low complexity), automatable and very flexible due to the composition of individual building blocks.

The following diagram shows a possible architecture for implementing a management layer as a standalone platform team that completely takes over the task of Azure Devops administration for a company.

The scaling effect begins when any amount of Azure DevOps projects can be managed in a fully automated manner for the business in a very short period of time, as shown in the following diagram.

You can find the full source code and more examples of the solution presented here on Github.

Use cases

To wrap up, I would like to describe a few real-world use cases as inspiration that can be very easily standardized and implemented.

As a CISO, I want to ensure that an implementation of the Azure DevOps authorization concept can be done effectively and fully without exceptions to comply with existing policies.

As a developer of a Java Spring Boot application, I would like to be provided with a standard DevOps build and deployment process that allows me to run my application based on the technology stack Maven + Cloud native build packs + Docker + Kubernetes so that I can focus on technical development.

As a cloud architect, I want to seamlessly integrate and audit Azure DevOps services into the company's multi-cloud strategy to build a consistent and flexible ecosystem.

Thomas WiessnerLinkedInAzure Cloud Architect
Related topics