„Managed DevOps Pools for Azure DevOps“ und wie diese unser Leben erleichtern

In diesem Beitrag stellen wir Azure DevOps Managed DevOps Pools vor und erklären, wie sie uns das Leben erleichtern. Wir vergleichen Managed DevOps Pools mit Self-Hosted Agents und zeigen, wie man diese Funktionalität vollständig mit Terraform automatisiert. Schließlich besprechen wir, wie man sie in der eigenen CI/CD-Pipeline verwendet.

Inhalt

Was versteht man unter Managed DevOps Pools for Azure DevOps?

Azure DevOps Agent-Pools sind eine Sammlung von Build- und Release-Agents, die verwendet werden, um CI/CD (Continuous Integration/Continuous Delivery)-Pipelines auf spezifischen Maschinen, virtuellen Maschinen oder Containern auszuführen. Die zugrunde liegende Infrastruktur für die Agent-Pools wird durch den neuen Service „Managed DevOps Pools for Azure DevOps“ von Microsoft bereitgestellt und verwaltet. Dieser Service automatisiert und vereinfacht nahezu alle Aufgaben rund um das Management der Agent-Pools und bietet den Endanwendern die größtmögliche Flexibilität in der Anpassung an die eigene Umgebung und an die Anforderungen.

Die Microsoft-gehostete Agents bieten keine Netzwerk-Integration, sodass man entweder auf das Hosting durch Microsoft verzichten oder den komplexen und wartungsintensiven Weg über Virtual Machine Scale Sets gehen musste um self-hosted Agents mit dem eigenen Netzwerk zu integrieren. Managed Pools for Azure DevOps löst diese Diskrepanz und vereint nun beides – Hosting und Management durch Microsoft sowie unzählige Anpassungsmöglichkeiten, inklusive der Anbindung an das eigene Netzwerk.

Wie funktioniert Managed DevOps Pools for Azure DevOps?

Auf der obigen Grafik sieht man ein vereinfachtes Setup eines Managed DevOps Pools im eigenen Netzwerk. Die Pipelines aus der Azure DevOps Organisation greifen auf den Managed DevOps Pool zu. Dieser läuft in der eigenen Subscription im privaten Netzwerk. Die eigentlichen Agents sind hierbei durch Microsoft abstrahiert.

Um die Kommunikation zu ermöglichen, bedarf es eines dedizierten Service Principals namens „DevOpsInfrastructure“ (Dieser wird beim Registrieren des Resource Providers Microsoft.DevOpsInfrastructure automatisch angelegt). Der Service Principal „DevOpsInfrastructure“ benötigt auf das virtuelle Netzwerk die Berechtigungen „Network Contributor“ sowie „Reader“. Weiterhin ist es möglich die notwendigen Rechte durch die Definition einer benutzerdefinierte Rolle zu reduzieren.

Was sind die Unterschiede zu den Self-Hosted Agents und zu den VMSS Agents?

Die Unterschiede zwischen den drei Optionen lassen sich in folgender Gegenüberstellung besonders gut erkennen:

 Self-hosted AgentsVMSS AgentsMDP Agents
InfrastrukturEigene VerantwortungEigene VerantwortungVerwaltung durch Microsoft
KontrolleVolle KontrolleVolle KontrolleKein Zugriff auf die zugrundeliegende VMs
SkalierungKeine SkalierungAzure Pipelines übernehmen die SkalierungVollautomatisierte Skalierung, sehr großer Pool an Agents
KostenVolle (Fix)Kosten für die InfrastrukturKosten für Compute und Parallel JobsKosten für Compute und Parallel Jobs
WartungEigene VerantwortungEigene VerantwortungWartung durch Microsoft

Welche Vorteile sehen wir im Einsatz von Azure Managed Pools?

Um zu erklären, warum wir Azure Managed Pools zum neuen Standard in unserem Projektgeschäft definiert haben, müssen wir etwas ausholen. Jedem Unternehmen ist es wichtig, dass auf seine privaten Ressourcen sicher zugegriffen wird. Den Entwicklerteams ist es wichtig, dass die Jobs und die Pipelines parallel und schnell laufen. Den Administratoren ist es wichtig, dass der Wartungsaufwand minimal bleibt. Den Platform Engineers ist es wichtig, dass das Setup rund um die Pipelines weitestgehend automatisiert abläuft.

Als wir die Ankündigung von Azure DevOps Managed Pools im Sommer 2024 gesehen und im späten Herbst über die allgemeine Freigabe gelesen haben, mussten wir es sofort ausprobieren – unsere Erwartungen wurden mehr als erfüllt. Wir sind nun in der Lage, vollautomatisiert via Terraform unseren Kunden Agents anzubieten, die mit wenig Aufwand sicher im Kundennetz laufen, nach Belieben skalieren können, komplett wartungsfrei sind.

Wie automatisiert man das Setup via Terraform am Beispiel von Managed Pool im eigenen Netzwerk?

Grundlegende Voraussetzungen sind eine Azure DevOps Organisation, ein Azure DevOps Projekt, eine administrativen Zugriff auf das Projekt und die Anbindung der Organisation an den Entra ID Dienst.

Zum Ausführen der Pipeline und zum Erstellen der Infrastruktur wird ein Service Principal verwendet. Der Service Principal sowie alles Notwendige für den Terraform State können mithilfe unseres Open-Source Projekt „Terraform scaffolding for Azure“angelegt werden (Weitere Informationen gibt es im zugehörigen Blogpost). Unser Terraform Scaffolding ist nach dem Prinzip der minimalen Berechtigungen (least privilege) aufgesetzt. Die meisten Anwendungsfälle sind mit den im Skript verwendeten minimalen Berechtigungen abgedeckt. Im Fall von Managed Pools muss im Entra ID dem Service Principal das Recht „Application.ReadAll“ zugewiesen werden.

Sobald die Voraussetzungen erfüllt sind (DevOps Organisation, Projekt, Service Principal, Terraform State und ein Terraform-Projekt, z.B. in VS Code), muss man als Erstes die notwendigen Resource Providers: „Microsoft.DevOpsInfrastructure“ und „Microsoft.DevCenter“ definieren.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "4.17.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = "2.2.0"
    }
    azurecaf = {
      source  = "aztfmod/azurecaf"
      version = "1.2.28"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "3.1.0"
    }
  }

  backend "azurerm" {
  }
  required_version = "1.10.5"
}
provider "azuread" {
}

provider "azurerm" {
  features {}
  subscription_id                 = "XYZ"
  resource_provider_registrations = "core"
  resource_providers_to_register = [
    "Microsoft.DevOpsInfrastructure",
    "Microsoft.DevCenter"
  ]
}

provider "azapi" {
}

Beim Registrieren von Microsoft.DevOpsInfrastructure wird in EntraID eine Enterprise Application mit dem Namen „DevOpsInfrastructure” angelegt.

Da unser Managed Pool in unserem privaten Azure Netzwerk laufen sollen, müssen wir dem „DevOpsInfrastructure“ Service Principal entsprechende Rechte zuweisen. Im ersten Schritt lesen wir den Service Principal ein:

data "azuread_service_principal" "managed_pool_sp" {
  display_name = "DevOpsInfrastructure"
}
data "azurerm_client_config" "current" {}

Dann legen wir vorbereitend eine variables.tf an in der wir die notwendigen Variablen definieren:

variable "project" {
  type    = string
  default = "poc"
}
variable "location" {
  type    = string
  default = "swedencentral"
}
variable "stage" {
  type    = string
  default = "dev"
}
variable "vnet_address_space" {
  type    = list(string)
  default = ["10.0.0.0/16"]
}
variable "subnet_address_space_runner" {
  type    = list(string)
  default = ["10.0.0.0/24"]
}
variable "azure_devops_organization" {
  type    = string
  default = "your-devops-org-name"
}
variable "azure_devops_project" {
  type    = string
  default = "your-devops-project-name"
}
variable "parallelism" {
  type    = number
  default = 2
}
variable "maximumConcurrency" {
  type    = number
  default = 2
}
variable "maxAgentLifetime" {
  type    = string
  default = "7.00:00:00"
}
variable "gracePeriodTimeSpan" {
  type    = string
  default = "00:00:00"
}
variable "runner_sku" {
  type    = string
  default = "Standard_B2ms"
}
variable "runner_state" {
  type    = string
  default = "Stateful"
}

Nun erstellen wir neben der Resource Group und dem VNet ein eigenes Subnet mit Service Delegation für die Agents des Managed Pools. Dieses Subnet wird ausschließlich vom Managed Pool for Azure DevOps verwendet (für alle unterstützten Ressourcen verwenden wir das Microsoft Cloud Adoption Framework (CAF), um die konsistente und klare Benennung der Azure-Ressourcen zu gewährleisten):

resource "azurecaf_name" "rg_core" {
  name          = var.project
  resource_type = "azurerm_resource_group"
  suffixes      = [var.stage, "core"]
}

resource "azurecaf_name" "vnet" {
  name          = var.project
  resource_type = "azurerm_virtual_network"
  suffixes      = [var.stage, "core"]
}

resource "azurecaf_name" "subnet" {
  name          = var.project
  resource_type = "azurerm_subnet"
  suffixes      = [var.stage]
}

locals {
  tags = {
    environment = var.stage
    location    = var.location
    managedBy   = data.azurerm_client_config.current.client_id
  }
  devOpsInfrastructureUser_object_id = data.azuread_service_principal.managed_pool_sp.object_id
}

resource "azurerm_resource_group" "core" {
  name     = azurecaf_name.rg_core.result
  location = var.location
  tags     = local.tags
}

resource "azurerm_virtual_network" "core" {
  name                = azurecaf_name.vnet.result
  address_space       = var.vnet_address_space
  location            = azurerm_resource_group.core.location
  resource_group_name = azurerm_resource_group.core.name
  tags                = local.tags
}

resource "azurerm_subnet" "runner" {
  name                 = "${azurecaf_name.subnet.result}-runner"
  resource_group_name  = azurerm_resource_group.core.name
  virtual_network_name = azurerm_virtual_network.core.name
  address_prefixes     = var.subnet_address_space_runner
 
  delegation {
    name = "Microsoft.DevOpsInfrastructure/pools"

    service_delegation {
      actions = [
        "Microsoft.Network/virtualNetworks/subnets/join/action",
      ]
      name = "Microsoft.DevOpsInfrastructure/pools"
    }
  }
}

In diesem Schritt berechtigen wir den Service Principal “DevOpsInfrastructure“ auf unserem privaten Azure Netzwerk:

resource "azurerm_role_assignment" "vnet_network_contributor_doi_user" {
  scope                = azurerm_virtual_network.core.id
  role_definition_name = "Network Contributor"
  principal_id         = local.devOpsInfrastructureUser_object_id
}

resource "azurerm_role_assignment" "vnet_reader_doi_user" {
  scope                = azurerm_virtual_network.core.id
  role_definition_name = "Reader"
  principal_id         = local.devOpsInfrastructureUser_object_id
}

Als Nächstes müssen wir die Ressourcen DevCenter und DevCenter Projekt anlegen da diese die Grundlage für den Managed DevOps Pool darstellen:

resource "azurerm_dev_center" "core" {
  name                = "dc-${var.project}-${var.stage}"
  location            = azurerm_resource_group.core.location
  resource_group_name = azurerm_resource_group.core.name
  tags      = local.tags

}

resource "azurerm_dev_center_project" "core" {
  name                = "dcp-${var.project}-${var.stage}"
  dev_center_id       = replace(azurerm_dev_center.core.id, "devCenters", "devcenters")
  location            = azurerm_resource_group.core.location
  resource_group_name = azurerm_resource_group.core.name
  tags      = local.tags

}

Die eigentliche Terraform Ressource zum Anlegen von Managed DevOps Pool ist der Runner. Diesen müssen wir über den AZAPI Terraform Provider anlegen. Dokumentation zum Verwenden der Ressource findet man hier.

resource "azapi_resource" "runner" {
  parent_id = azurerm_resource_group.core.id
  type      = "Microsoft.DevOpsInfrastructure/pools@2024-10-19"
  name      = "mdp-${var.project}-${var.stage}"
  location  = azurerm_resource_group.core.location
  tags      = local.tags
  body = {
    properties = {
      organizationProfile = {
        organizations = [
          {
            url = "https://dev.azure.com/${var.azure_devops_organization}"
            projects = [
              "${var.azure_devops_project}"
            ],
            parallelism = var.parallelism
          }
        ],
        permissionProfile = {
          kind = "Inherit"
        }
        kind = "AzureDevOps"
      }
      devCenterProjectResourceId = azurerm_dev_center_project.core.id
      maximumConcurrency         = var.maximumConcurrency
      agentProfile = {
        maxAgentLifetime    = var.maxAgentLifetime
        gracePeriodTimeSpan = var.gracePeriodTimeSpan
        kind                = var.runner_state
        resourcePredictionsProfile = {
          predictionPreference = "Balanced"
          kind                 = "Automatic"
        }
      }

      fabricProfile = {
        sku = {
          name = var.runner_sku
        }
        images = [
          {
            aliases = [
              "ubuntu-22.04"
            ]
            buffer             = "*"
            wellKnownImageName = "ubuntu-22.04/latest"
          }
        ]
        osProfile = {
          secretsManagementSettings = {
            observedCertificates = []
            keyExportable        = false
          }
          logonType = "Service"
        }
        storageProfile = {
          osDiskStorageAccountType = "StandardSSD"
          dataDisks                = []
        }
        networkProfile = {
          subnetId = azurerm_subnet.runner.id
        }
        kind = "Vmss"
      }
      provisioningState = "Succeeded"
    }
  }
}

An dieser Stelle sind wir mit den Terraform-Aufgaben durch und wechseln ins Azure DevOps Portal. Da wir die Ressourcen über die Pipeline anlegen wollen, brauchen wir als Erstes eine Service Connection. Diese legen wir unter Verwendung des Service Principals aus dem ersten Schritt an (zur Erinnerung: die Vorgehensweise beim Anlegen des Terraform-Grundgerüsts inklusive Service Principal und Terraform State Management findet man unter folgendem Link. Natürlich würden sich die notwendigen Anpassungen im Azure DevOps Portal auch mit Terraform über den Azure DevOps Provider realisieren lassen.

Als Nächstes müssen wir dem Nutzer, in dessen Kontext die Pipeline läuft, Berechtigungen erteilen. In unserem Fall ist dies der bereits bekannte Service Principal. Dieser manuelle Schritt erfolgt vor dem Ausführen der Pipeline zum Anlegen des Managed Pools:

  1. Der Service Principal soll als „User“ zur „Organization“ hinzugefügt werden.
  2. Dem Service Principal wird anschließend die „Project Administrator“-Berechtigung auf das Projekt erteilt.
  3. Unter „Organisation settings“ → „Pipelines“ → „Agent Pools“ muss der Service Principal als Administrator hinzugefügt werden. (Die Berechtigungen können nach dem initialen Ausführen zum Anlegen des Pools wieder entzogen werden.)

Es ist alles bereit, um die Pipeline erstmalig laufen zu lassen. Die Pipeline ist eine einfache YAML-Pipeline, die unter anderem Terraform plan und Terraform apply ausführt. Da beim initialen Lauf der Managed Pool noch nicht vorhanden ist und erst angelegt werden muss, lassen wir die Pipeline mit einem Standard Microsoft Hosted Agent laufen:

pool:
  vmImage: ubuntu-latest

Das Ergebnis sehen wir direkt im Azure Portal:

Der finale Schritt ist das Austauschen des zu verwendenden Pools in der Pipeline:

pool:
   name: mdp-poc-dev

Ab jetzt läuft die Pipeline im Microsoft Managed Pool, der sich in unserem privaten Netzwerk befindet. Wir sind nun bereit, alle Features der Managed DevOps Pools für Azure DevOps voll auszunutzen.

Sobald die Agents im Pool einen Job ausgeführt haben, hat man im Azure Portal die Möglichkeit, die Ausführung des Jobs auszuwerten sowie weitere Metriken und mögliche Fehler zu analysieren. Als Einstieg benutzt man die Overview-Seite der Ressource „Managed DevOps Pool“:

Welche Limitierungen gibt es bei Managed DevOps Pools?

  1. Kein Fernzugriff: Was bei den Self-Hosted Agents ein Vorteil ist, könnte bei dem Einsatz von Managed DevOps Pools ein Nachteil sein – die virtuellen Maschinen sind für mögliche Anpassungen nicht erreichbar, da sie komplett in der Verantwortung von Microsoft liegen.
  2. Begrenzte Anpassungsmöglichkeiten: da die Infrastruktur von Microsoft verwaltet werden, hat man im Vergleich zu selbst gehosteten Agenten nur begrenzte Kontrolle über deren Konfiguration
  3. Regionale Verfügbarkeit: Managed DevOps Pools sind nur in bestimmten Azure-Regionen verfügbar. Vor dem Einsatz überprüfen werden, ob die gewünschte oder im Projekt vorausgesetzte Region verfügbar ist. Dafür öffnet man in der Subscription Overview “Resource Providers”, sucht nach Microsoft.DevOps, klickt auf Microsoft.DevOpsInfrastructure und kontrolliert im Tab “Location”, ob die notwendige Region auswählbar ist:

4. Quotenbeschränkungen: neben der regionalen Verfügbarkeit sollte man beachten, dass es Standardquoten für die Anzahl der Agenten gibt, die man erstellen kann. Möglicherweise muss eine Erhöhung beantragt werden, wenn der Bedarf diese Grenzen überschreitet. Wie man die Erhöhung beantragt, ist hier beschrieben.

Zusammengefasst bieten Managed DevOps Pools for Azure DevOps eine zentralisierte und vereinfachte Lösung zur Verwaltung von Agenten für CI/CD-Pipelines. Diese Pools ermöglichen eine sichere und automatisierte Integration in private Netzwerke und minimieren gleichzeitig den Wartungsaufwand durch Microsofts Management der zugrunde liegenden Infrastruktur. Trotz einiger Limitierungen bezüglich Fernzugriff und Anpassungsmöglichkeiten, bieten Managed DevOps Pools eine flexible und skalierbare Lösung, die besonders in komplexen Unternehmensumgebungen von Vorteil ist.

Sie möchten mehr über Managed DevOps Pools for Azure erfahren oder diese direkt in Ihren Projekten nutzen? Sprechen Sie uns an!