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.

Polat

Polat

Hallo, mein Name ist Polat. Geboren und aufgewachsen in der Nähe von Stuttgart bin ich über Umwege in Istanbul gelandet. Zusammen mit meiner Kamera reise ich so oft es geht. Meine Eindrücke und Erlebnisse möchte ich in diesem Blog festhalten und somit dem einen oder anderen Reisetipps für die eigene Planung geben.