Nahaufnahme einer Person, die mit einem Tablet auf einer Decke sitzt.

TechWiese Blog

Einführung in den Azure DevOps Terraform Provider

12. August 2020

Portrait Bild von Christian Dennig

Christian Dennig

Dieser Blogbeitrag ist ein Repost und stammt im Original aus dem Know-how-Bereich von TechWiese, dessen Artikel in diesem Blog aufgegangen sind.

Vor nicht allzu langer Zeit wurde der Azure DevOps Terraform Provider in einer ersten Version veröffentlicht. In diesem Beitrag zeige ich anhand verschiedener Beispiele, welche Features momentan in Bezug auf Pipeline supportet werden und wie man den Provider verwendet, u.a. auch in Verbindung mit Azure. Der Provider ist für viele, die sich im Bereich "Infrastructure As Code" bewegen, der letzte Baustein, um Umgebungen (inkl. Git Repos, Service Connections, Build + Release Pipelines etc.) vollständig automatisch zu erstellen.

Der Provider wurde im Juni 2020 in der Version 0.0.1 veröffentlicht, aber soviel sein schonmal gesagt: der Feature Umfang ist in dem frühen Stadium schon recht umfangreich.

Die Funktionen, auf die ich anhand von Beispielen eingehen möchte, sind folgende:

  • Anlage eine DevOps Projects inkl. eines gehosteten Git Repos
  • Anlage einer Build-Pipeline
  • Verwendung von Variablen und Variablen-Gruppen
  • Anlage einer Azure Service Connection und Verwendung von Variablen/Secrets aus einem Azure KeyVault

Beispiel 1: Grundlegende Verwendung

Der Azure DevOps Provider lässt sich wie jeder andere Terraform Provider in ein Skript einbinden. Man muss lediglich zusätzlich die URL zur DevOps Organisation und ein Personal Access Token (PAT) hinterlegen, mit dem sich der Provider gegenüber Azure DevOps authentifizieren kann.

Das PAT an sich lässt sich unkompliziert über die UI von Azure DevOps erstellen, in dem man über User Settings --> Personal Access Token --> New Token ein neues Token erstellt. Der Einfachheit halber, gebe ich in diesem Beispiel "Full Access" - dies sollte man natürlich für die eingenen Zwecke entsprechend anpassen.

Die Dokumentation des Terraform Providers enthält Hinweise zu den Berechtigungen, die für die jeweilige Ressource benötigt werden.

Personal Access Token

Hat man das Access Token erstellt, kann der Azure DevOps Provider im Terraform Skript folgendermaßen referenziert werden:

provider"azuredevops" {
  version               = ">= 0.0.1"
  org_service_url       = var.orgurl
  personal_access_token = var.pat
}
							

Die beiden Variablen orgurl und pat hinterlegen man am besten als Umgebungsvariablen:

$ export TF_VAR_orgurl = "https://dev.azure.com/<ORG_NAME>"
$ export TF_VAR_pat = "<PAT_AUS_AZDEVOPS>"

Damit hat man die Grundlage gelegt, um mit Terraform gegen Azure DevOps zu arbeiten. Legen wir also ein neues Projekt und ein Git Repository an. Zwei Ressource sind dafür notwendig, azuredevops_project und azuredevops_git_repository:

resource"azuredevops_project""project" {
  project_name       = "Terraform DevOps Project"
  description        = "Sample project to demonstrate AzDevOps <-> Terraform integragtion"
  visibility         = "private"
  version_control    = "Git"
  work_item_template = "Agile"
}

resource"azuredevops_git_repository""repo" {
  project_id = azuredevops_project.project.id
  name       = "Sample Empty Git Repository"

  initialization {
    init_type = "Clean"
  }
}
							

Dazu kommt eine initiale Pipeline, die bei einem Push in den master Branch gestartet wird.

In einer Pipeline wird in der Regel mit Variablen gearbeitet, die aus unterschiedlichen Quellen stammen. Das können Pipeline Variablen sein, Werte aus einer Variablengruppe oder aus externen Quellen wie z.B. einem Azure KeyVault. Die erste, einfache Build Definition verwendet hierbei Pipeline Variablen (mypipelinevar):

resource"azuredevops_build_definition""build" {
  project_id =  azuredevops_project.project.id
  name       = "Sample Build Pipeline"
  
  ci_trigger {
    use_yaml =true
  }

repository {
  repo_type   = "TfsGit"
  repo_id     =  azuredevops_git_repository.repo.id
  branch_name =  azuredevops_git_repository.repo.default_branch
  yml_path    = "azure-pipeline.yaml"
}

variable {
  name      = "mypipelinevar"
  value     = "Hello From Az DevOps Pipeline!"
  is_secret = false
  }
}

Die dazugehörige Pipeline-Definition sieht folgendermaßen aus:

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

steps:
- script: echo Hello, world!
  displayName: 'Run a one-line script'

- script: |
    echo Pipeline is running!
    echo And here is the value of our pipeline variable
    echo $(mypipelinevar)
  displayName: 'Run a multi-line script'

Die Pipeline führt dabei lediglich Skripte aus - rein zu Demo-Zwecken - und gibt die in der Definition hinterlegte Variable auf der Console aus.

Führt man das Terraform Skript aus, werden ein Azure DevOps Projekt, ein Git Repository und eine Build Definition angelegt.

DevOps Projekt

DevOps Git Repo

DevOps Pipeline

Pusht man das oben besprochene File azure_pipeline.yaml in das Repo, wird die entsprechende Pipeline getriggert und im jeweiligen Build Step werden die Ergebnisse ausgegeben:

DevOps Pipeline

DevOps Pipeline

Beispiel 2: Verwendung von Variablen-Gruppen

Normalerweise werden Variablen nicht direkt in einer Pipeline Definition hinterlegt, sondern man verwendet Azure DevOps Variablengruppen. Dadurch hat man die Möglichkeit, einzelne Variablen zentral in Azure DevOps abzulegen und diese dann in unterschiedlichen Pipelines zu referenzieren und zu verwenden.

Dabei lassen sich Variablegruppen ebenfalls per Terraform anlegen. Dazu verwendet man die Ressource azuredevops_variable_group. In unserem Skript sieht dies folgendermaßen aus:

resource"azuredevops_variable_group""vars" {
  project_id   =  azuredevops_project.project.id
  name         = "my-variable-group"
  allow_access = true

  variable {
    name  = "var1"
    value = "value1"
  }

  variable {
    name  = "var2"
    value = "value2"
  }
}

resource"azuredevops_build_definition""buildwithgroup" {
  project_id =  azuredevops_project.project.id
  name       = "Sample Build Pipeline with VarGroup"

  ci_trigger {
    use_yaml = true
  }

  variable_groups =  [
    azuredevops_variable_group.vars.id
  ]

repository {
  repo_type   = "TfsGit"
  repo_id     =  azuredevops_git_repository.repo.id
  branch_name =  azuredevops_git_repository.repo.default_branch
  yml_path    = "azure-pipeline-with-vargroup.yaml"
  }

}

Der erste Teil des Terraform Skripts legt die Variablengruppe in Azure DevOps (Name: my-variable-group) inkl. zweier Variablen (var1 und var2) an, der zweite Teil - eine Build Definition - verwendet die Variablengruppen, so dass man im entsprechenden Pipeline-File darauf zugreifen (azure-pipeline-with-vargroup.yaml) kann.

Dieses hat folgenden Inhalt:

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

variables:
- group: my-variable-group

steps:
- script: echo Hello, world!
  displayName: 'Run a one-line script'

- script: |
    echo Var1: $(var1)
    echo Var2: $(var2)
  displayName: 'Run a multi-line script'

Lässt man zunächst das Terraform Skript laufen, werden die entsprechenden Azure DevOps Objekte erzeugt - eine Variablengruppe und eine Pipeline.

DevOps Pipeline

Pusht man nun die oben angegebene Build YAML Definition in das Repo, wird die Pipeline ausgeführt und auf der Console sollte die beiden Werte aus den hinterlegten Variablen ausgegeben werden.

DevOps Pipeline

Beispiel 3: Verwendung von Azure KeyVault und Azure DevOps Service Connections

Aus Security-Gründen legt man kritische Werte weder direkt in einer Pipeline-Definition, noch in Azure DevOps in Variablengruppen ab. Man verwendet einen externen Vault wie Azure KeyVault. Glücklicherweise hat man mit Azure DevOps die Möglichkeit, direkt auf einen bestehenden KeyVault zuzugreifen und Werte auszulesen, die einem dann als Variablen innerhalb der eigenen Pipeline zur Verfügung gestellt werden.

Natürlich muss man Azure DevOps hierfür gegenüber Azure authentifizieren/authorisieren. Dafür gibt es in Azure DevOps das Konzept der Service Connections. Service Connections werden verwendet, um z.B.auf Bitbucket, GitHub, Jira, Jenkis...oder eben auch auf Azure zuzugreifen. Man hinterlegt einen zentralen User - im Fall von Azure ist dies ein Service Principal - der von Pipelines zur Durchführung verschiedener Aktionen verwendet wird - in unserem Beispiel das Auslesen eines Secrets aus einem KeyVault.

Um dieses Szenario zu demonstrieren, müssen zunächst auf Azure verschiedene Dinge eingerichtet werden:

  • Anlage einer Application / eines Service Principals im Azure Active Directory, der von Azure DevOps zur Authentifizierung verwendet wird
  • Anlage eines Azure KeyVaults (inkl. einer Resource Group)
  • Berechtigung des Service Principals auf den Azure KeyVault, um secrets auslesen zu können (keine Schreibrechte!)
  • Anlage eines Secrets zur Verwendung in einer Variablengruppe / Pipeline

Terraform bietet mit dem Azure Provider die Möglichkeit, Azure Ressourcen zu verwalten. Dieser wird im Folgenden verwendet, um die oben genannten Ressourcen zu erzeugen.

AAD Application + Service Principal

Zunächst einmal benötigt man einen Service Principal, der von Azure DevOps verwendet werden kann, um sich gegenüber Azure zu authentifizieren. Das dazugehörige Terraform Skript sieht wie folgt aus:

data"azurerm_client_config""current" {
}

provider"azurerm" {
  version ="~> 2.6.0"
  features {
    key_vault {
      purge_soft_delete_on_destroy =true
    }
  }
}

## Service Principal for DevOps

resource"azuread_application""azdevopssp" {
  name ="azdevopsterraform"
}

resource"random_string""password" {
  length  = 24
}

resource"azuread_service_principal""azdevopssp" {
  application_id = azuread_application.azdevopssp.application_id
}

resource"azuread_service_principal_password""azdevopssp" {
  service_principal_id =  azuread_service_principal.azdevopssp.id
  value                =  random_string.password.result
  end_date             = "2024-12-31T00:00:00Z"
}

resource"azurerm_role_assignment""contributor" {
  principal_id         =  azuread_service_principal.azdevopssp.id
  scope                = "/subscriptions/${data.azurerm_client_config.current.subscription_id}"
  role_definition_name = "Contributor"
}

Mit dem oben dargestellten Skript wird sowohl eine AAD Application, als auch ein Service Principal generiert. Dabei ist zu beachten, dass dem Service Principal die Rolle Contributor zugewiesen wird - und zwar auf Subscription Ebene, siehe scope- Zuweisung. Dies sollte in eigenen Projekten entsprechend eingeschränkt werden (z.B. auf die jeweilige Resource Group)!

Azure KeyVault

Die Anlage des KeyVault wird analog zu den vorherigen Ressourcen durchgeführt. Dabei ist zu beachten, dass dem User mit dem gegen Azure gearbeitet wird, volle Berechtigungen auf die Secrets im KeyVault gegeben wird. Weiter unten im Skript, werden auch die Permissions für den Azure DevOps Service Principals innerhalb des KeyVaults vergeben - hier jedoch ausschließlich Leserechte! Zu guter Letzt wird auch noch ein entsprechendes Secret kvmysupersecretsecret angelegt, mit dem wir die Integration testen können.

resource"azurerm_resource_group""rg" {
  name     = "myazdevops-rg"
  location = "westeurope"
}

resource"azurerm_key_vault""keyvault" {
  name                        = "myazdevopskv"
  location                    = "westeurope"
  resource_group_name         =  azurerm_resource_group.rg.name
  enabled_for_disk_encryption = true
  tenant_id                   = data.azurerm_client_config.current.tenant_id
  soft_delete_enabled         = true
  purge_protection_enabled    = false

  sku_name = "standard"

  access_policy {
    tenant_id = data.azurerm_client_config.current.tenant_id
    object_id = data.azurerm_client_config.current.object_id

    secret_permissions = [
      "backup",
      "get",
      "list",
      "purge",
      "recover",
      "restore",
      "set",
      "delete",
    ]
    certificate_permissions = [
    ]
    key_permissions = [
    ]
  }

}

## Grant DevOps SP permissions

resource"azurerm_key_vault_access_policy""azdevopssp" {
  key_vault_id = azurerm_key_vault.keyvault.id

  tenant_id = data.azurerm_client_config.current.tenant_id
  object_id = azuread_service_principal.azdevopssp.object_id

  secret_permissions = [
    "get",
    "list",
  ]
}

## Create a secret

resource"azurerm_key_vault_secret""mysecret" {
  key_vault_id =  azurerm_key_vault.keyvault.id
  name         = "kvmysupersecretsecret"
  value        = "KeyVault for the Win!"
}

Hat man die oben beschriebenen Schritte ausgeführt, ist das Ergebnis in Azure ein neu erstellter KeyVault inkl. eines Secrets:

KeyVault

Service Connection

Nun benötigen wir die Integration in Azure DevOps, da wir schlussendlich auf das neu erstellte Secret in einer Pipeline zugreifen wollen. Azure DevOps hat von Haus aus die Möglichkeit, auf einen KeyVault und die darin enthaltenen Secrets zuzugreifen. Um dies zu ermöglichen, muss man - ohne Terraform - allerdings einige manuelle Schritte durchführen (u.a. Zugang zu Azure ermöglichen). Diese sind zum Glück nun mit Terraform automatisierbar. Mit den folgenden Ressourcen wird in Azure DevOps eine Service Connection auf Azure angelegt und unserem Projekt Zugriff gewährt:

## Service Connection

resource"azuredevops_serviceendpoint_azurerm""endpointazure" {
  project_id            = azuredevops_project.project.id
  service_endpoint_name = "AzureRMConnection"
  credentials {
    serviceprincipalid  =  azuread_service_principal.azdevopssp.application_id
    serviceprincipalkey =  random_string.password.result
  }
  azurerm_spn_tenantid      = data.azurerm_client_config.current.tenant_id
  azurerm_subscription_id   = data.azurerm_client_config.current.subscription_id
  azurerm_subscription_name = "dechrist - Microsoft Azure Internal Consumption"
}

## Grant permission to use service connection

resource"azuredevops_resource_authorization""auth" {
  project_id  = azuredevops_project.project.id
  resource_id = azuredevops_serviceendpoint_azurerm.endpointazure.id
  authorized  =true
}

Service Connection

Anlage eine Variablen-Gruppe und Pipeline Definition

Der letzte notwendige Schritt, um den KeyVault in einer Pipeline verwenden zu können, ist die Anlage eine entsprechenden Variablen-Gruppe und das "Verknüpfen" des bestehenden Secrets.

## Pipeline with access to kv secret

resource"azuredevops_variable_group""kvintegratedvargroup" {
  project_id   = azuredevops_project.project.id
  name         = "kvintegratedvargroup"
  description  = "KeyVault integrated Variable Group"
  allow_access = true

  key_vault {
    name                = azurerm_key_vault.keyvault.name
    service_endpoint_id = azuredevops_serviceendpoint_azurerm.endpointazure.id
  }

  variable {
  name    = "kvmysupersecretsecret"
  }
}

DevOps Var Group

Test-Pipeline

Alle Voraussetzungen sind nun geschaffen, fehlt noch eine Pipeline, mit der wir testen können.

Skript für die Anlage der Pipeline:

resource"azuredevops_build_definition""buildwithkeyvault" {
  project_id =  azuredevops_project.project.id
  name       = "Sample Build Pipeline with KeyVault Integration"

  ci_trigger {
    use_yaml = true
  }

  variable_groups = [
  azuredevops_variable_group.kvintegratedvargroup.id
  ]


  repository {
    repo_type   = "TfsGit"
    repo_id     =  azuredevops_git_repository.repo.id
    branch_name =  azuredevops_git_repository.repo.default_branch
    yml_path    = "azure-pipeline-with-keyvault.yaml"
  }
}

Pipeline-Definition (azure-pipeline-with-keyvault.yaml):

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

variables:
- group: kvintegratedvargroup

steps:
- script: echo Hello, world!
  displayName: 'Run a one-line script'

- script: |
    echo KeyVault secret value: $(kvmysupersecretsecret)
  displayName: 'Run a multi-line script'

Hat man das Terraform Skript laufen lassen und die Pipeline im Repo eingecheckt, erhält man im darauffolgenden Build folgendes Ergebnis (Secret wird aus Sicherheitsgründen natürlich nicht ausgegeben!):

DevOps Pipeline mit KeyVault

Zusammenfassung

Die Einrichtung von neuen Azure DevOps Projekten war nicht immer die einfachste Aufgabe, da man teilweise manuelle Schritte durchführen musste. Mit dem Release der ersten Terraform Provider Version für Azure DevOps hat sich dies fast schon dramatisch geändert :) Man kann nun - als einen der letzten Bausteine für die Automatisierung - viele Dinge per Terraform erledigen. Im hier gezeigten Beispiel wurde schlussendlich der Zugriff auf einen Azure KeyVault inkl. der Anlage der Service Connection durchgeführt. Damit ist jedoch nur ein Baustein gezeigt worden - wenn auch einer, der mich in regelmäßigen Abständen "geärgert hat", da man das meiste manuell einrichten musste. Der Provider kann unter anderem auch Branch Policies verwalten, Gruppen und Gruppenmitgliedschaften einrichten etc. Mit der ersten Version steht man hier natürlich noch recht weit am Anfang, aber es ist aus meiner Sicht ein guter Start, mit dem man schon sehr viel erreichen kann. Ich bin gespannt, was als nächstes supportet werden wird.

Alle Unterlagen (Terraform Skripte + Pipelines) sind hier zu finden: https://github.com/azuredevcollege/devops/tree/master/tf-devops

Hinweis: Dieser Post ist ursprünglich auf Englisch auf dem Blog des Authors erschienen.