Terraform, GitLab und Azure

In diesem Post geht es um Terraform, GitLab und Azure, und das Zusammenspiel. Ich habe Terraform Skripte erstellt, und diese per git nach GitLab hochgeladen. Bei jedem Hochladen wollte ich, dass eine Pipeline startet, die die Ressourcen in Azure dann erstellt. Ich beschreibe, wie das geht.

Terraform

Zunächst habe ich ein einfaches Projekt erstellt mit einem Terraform-Skript main.tf.
Darin ist der Provider enthalten. In diesem Fall Azure von Hashicorp.

terraform {
required_providers {
azurerm = {
source  = "hashicorp/azurerm"
version = "=3.0.0"
}
}
}
provider "azurerm" {
features {}
}

Um das Passwort und den Admin der zu erstellenden VM nicht im Klartext zu haben, habe ich Variablen erstellt.

variable "azure_vm_user_name" {
type = string
}
variable "azure_vm_user_password" {
type = string
}

Anschließend müssen die Ressourcen codiert werden. Dazu zunächst eine Ressourcegroup erstellen.

resource "azurerm_resource_group" "pkrg" {
name     = "${terraform.workspace}-rg"
location = "West Europe"
}

Im Anschluss werden Netzwerk relevante Ressourcen angelegt.

resource "azurerm_virtual_network" "pknetwork" {
name                = "${terraform.workspace}-network"
address_space       = ["10.0.0.0/16"]
location            = azurerm_resource_group.pkrg.location
resource_group_name = azurerm_resource_group.pkrg.name
}
resource "azurerm_subnet" "pksubnet" {
name                 = "${terraform.workspace}-subnet"
resource_group_name  = azurerm_resource_group.pkrg.name
virtual_network_name = azurerm_virtual_network.pknetwork.name
address_prefixes     = ["10.0.2.0/24"]
}
resource "azurerm_network_interface" "pkinterface" {
count               = 4
name                = "${terraform.workspace}-nic${count.index}"
location            = azurerm_resource_group.pkrg.location
resource_group_name = azurerm_resource_group.pkrg.name
ip_configuration {
name                          = "internal"
subnet_id                     = azurerm_subnet.pksubnet.id
private_ip_address_allocation = "Dynamic"
}
}

Als letztes werden die VMs erstellt. count gibt an, wie oft die Ressource angelegt werden soll. ${terraform.workspace} ist ein dynamischer Wert, der per Default default ist. Es ist ratsam, pro Umgebung einen Workspace anzulegen. Man kann diese Variable auch nutzen, um Tags zu setzen. ${count.index} ist die laufende Variable zu count

resource "azurerm_windows_virtual_machine" "createVMs" {
count                 = 4
name                  = "${terraform.workspace}-vm-${count.index}"
resource_group_name   = azurerm_resource_group.pkrg.name
location              = azurerm_resource_group.pkrg.location
size                  = "Standard_B1s"
admin_username        = var.azure_vm_user_name
admin_password        = var.azure_vm_user_password
network_interface_ids = [element(azurerm_network_interface.pkinterface.*.id, count.index)]
os_disk {
caching              = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer     = "WindowsServer"
sku       = "2016-Datacenter"
version   = "latest"
}
tags = {
Environment = "${terraform.workspace}"
}
}

GitLab

Damit GitLab ein Projekt erkennt, muss im Hauptverzeichnis eine .gitlab-ci.yml erstellt werden. Hier wird die Pipeline mit den Stages definiert. Der Code ist einfach gehalten und ist nicht optimiert. Er dient lediglich dazu, zu zeigen, wie man relativ schnell eine Pipeline zum Ausrollen der Infrastruktur auf Azure automatisieren kann.

Der cache Block ist dafür da, dass GitLab Dateien aus einem vorherigen Schritt nicht mehrmals erzeugen muss, womit der Prozess insgesamt schneller wird. Der before_script Block wird vor jedem script Block ausgeführt. Das ist hier sicher nicht optimal, aber der Einfachheit geschuldet. Mit auto-approve verhindert man die manuelle Eingabe von 'yes' beim Ausführen von terrafom apply .

image:
name: hashicorp/terraform:1.3.6
entrypoint: [""]
stages:
- infra:plan
- infra:apply
cache:
key: tf-cache
paths:
- ${TF_ROOT}/.terraform
before_script:
- terraform --version 
- Set-Item -Path env:TF_VAR_azure_vm_user_name -Value $TF_VAR_azure_vm_user_name
- Set-Item -Path env:TF_VAR_azure_vm_user_password -Value $TF_VAR_azure_vm_user_password
- az login --service-principal -u $TF_VAR_azure_principal_client_id -p $TF_VAR_azure_principal_password --tenant $TF_VAR_azure_tenant_id
plan_infra:
stage: infra:plan
script:
- terraform init  
- terraform workspace new dev
- terraform workspace select dev
- terraform plan -out=tfplan
artifacts:
paths:
- tfplan
untracked: false
when: on_success
expire_in: "1 days"
apply_infra:
stage: infra:apply
script:
- terraform init  
- terraform workspace new dev
- terraform workspace select dev
- terraform apply -auto-approve "tfplan"
dependencies: 
- plan_infra
when: manual 

Secrets (Variablen)

Um sensible Variablen wie Passwörter aus GitLab nach Terraform durchzureichen, muss man diese als Umgebungsvariablen definieren. Wieso Windows mag sich der ein oder andere fragen. Das liegt daran, dass GitLab lediglich 400 Requests kostenfrei erlaubt, wenn man seine Kreditkarten hinterlegt. Da ich meine Kreditkartendaten nicht hinterlegen wollte, habe ich einen lokalen GitLab Runner unter Windows erstellt.

GitLab Variablen werden im Projekt unter Settings > CI/CD angelegt. Da ich direkt auf nach master deploye, musste ich bei den Variablen den Flag Protected deaktivieren.

In der .gitlab-ci.yml muss man diese Variablen dann als Umgebunsvariable deklarieren:

- Set-Item -Path env:TF_VAR_azure_vm_user_name -Value $TF_VAR_azure_vm_user_name
- Set-Item -Path env:TF_VAR_azure_vm_user_password -Value $TF_VAR_azure_vm_user_password

Im dritten Schritt muss man die Definition der Variablen in Terraform anlegen.

variable "azure_vm_user_name" {
type = string
}
variable "azure_vm_user_password" {
type = string
}

Im letzten Schritt kann man in Terraform im Ressource Block auf die Variable zugreifen.

admin_username = var.azure_vm_user_name
admin_password = var.azure_vm_user_password

Azure

Es ist nicht ratsam, die Admin-Credentials für die Verbindung zwischen einem Automaten mit Azure zu nutzen. Bei mir hat das auch erst gar nicht geklappt, so dass ich zunächst einen Service Principal mit PowerShell erzeugen musste. Die im Anschluss angezeigten Werte habe ich dann als GitLab-Variablen gespeichert.

az ad sp create-for-rbac --name <service_principal_name> --role Contributor --scopes /subscriptions/<subscription_id>

Das Verbinden mit Azure aus der Pipeline funktioniert dann wie folgt:

az login --service-principal -u $TF_VAR_azure_principal_client_id -p $TF_VAR_azure_principal_password --tenant $TF_VAR_azure_tenant_id

Issues

Der Weg (in der IT) ist immer steinig. Ich kann mich kaum an eine initiale Erstellung einer Umgebung erinnern, die reibungsfrei funktioniert hätte. Meist stößt man auf Restriktionen und Abhängigkeiten, die man erst lösen muss, ehe man über weite Umwege zur eigentlichen Entwicklung oder Problemstellung kommt.

Der lokale GitLab Runner hat mich einige Zeit gekostet, da Windows beim Registrieren des Runners rumgezickt hat. Aber was soll ich sagen, bei Windows hilft es, den Rechner durchzustarten.

Das Verbinden zwischen Terraform und Azure hat mich auch einiges an Recherche gekostet, bis ich den ServicePrincipal angelegt und az login angepasst habe.

Leider habe ich wenig Quellen darüber gefunden, wie man GitLab-Variablen nach Terraform durchreichen kann. Die wenigen Quellen haben dann auch nicht funktioniert. Bis ich erkannt habe, dass zum einen natürlich die export-Funktion auf dem Windows-Runner nicht existiert, und zum anderen die Syntax sich etwas geändert hat, und man export auch nicht mit set ersetzen kann.