Eine komplette Terraform Pipeline mit Azure DevOps


Einführung




Für das Provisioning von Ressourcen auf Azure ist Terraform eine gute Wahl. Ich verweise auf meinen Blog "Azure Infrastruktur erstellen mit Terraform". In diesem Blog gebe ich Hinweise, wie ein Projekt aufgebaut werden kann, so dass effektiv und effizient Code mit HCL - das ist die Programmiersprache die hinter Terraform steckt - entwickelt werden kann. Dazu werden zuerst die notwendigen Einstellungen im Azure Portal diskutiert und anschliessend, wie der Code im Webbrowser mit Azure DevOps automatisch, mittels einer Pipeline, auf einen Provisioning Agent deployt werden kann. Dieser erstellt oder löscht dann die Ressourcen auf Azure.

Azure Cloud Shell



Die Azure Cloud Shell ist eine Shell, die direkt im Webbrowser geöffnet wird. In dieser Shell können wie in einer normalen Linux Shell Kommandos abgesetzt werden. Eine Shell steht nicht alleine da - im Hintergrund muss sich irgendwo, eine Maschine befinden, auf derer die Kommandos ausgeführt werden können. Wie ungefähr das in Azure realisert wird, kann durch folgende Beobachtungen herausgefunden werden.

Beim Öffnen der Shell wird eine Ressourcengruppe angelegt:


Die Shell Ressourcengruppe


Die Ressourcengruppe enthält einen Storage-Account:

Storage Account der Shell


In diesem Storage Account kann dann mittels des Storage Explorers herausgefunden werden, was in diesem Storage Account gespeichert ist. Hier ist es eine Fileshare mit einer Image Datei:


Der Inhalt besteht nur aus einem File-Share



Die Image Daei für die Cloud Shell



Die Image Datei wird wahrscheinlich ein Image sein, das auf einem Agenten ausgeführt werden kann, so dass der Agent wie ein gewöhnlicher Computer reagiert der nur über eine Shell oder aber über eine cmd-Konsole verfügt.

Die Cloud Shell persistiert die Eingaben während den verschiedenen Sitzungen. Das heisst wird eine Textdatei in einer Sitzung erzeugt, so ist sie dann auch in der nächsten Sitzung vorhanden. Die Cloud Shell hat zudem recht viele Systemtools (Linux, Windows) schon integriert. So ist auch Terraform Bestandteil der Cloud Shell.

Für erste Versuch mit Terraform kann direkt mit der Cloud Shell gearbeitet werden. Angenommen der Code sei in einem GIT Repository gespeichert. Dann kann mitltels des Kommandos

 git pull https://... 

der Code in ein beliebiges Verzeichnis in der Shell kopiert werden. Terraform kann dann sofort auf den Code angewendet werden.

Provisioning auf einem Agenten


Die Azure Cloud Shell ist zwar gut geeignet für das Ausprobieren von Terraform Code, jedoch ist es keine Automatisierungslösung. Für die Automatisierung verfügt Azure über Pools von Rechnern, die kurfristig für die Ausführung von Code verwendet werden können. Die Pools unterscheiden sich durch das installierte Betriebssystem und der darauf vorhandenen vornstallierten Programme. So gibt es reine Windows Pools aber auch Linux Pools. Ein Rechner aus einem dieser Pools fungiert dann als Agent. Der Agent zieht den Code aus einem Repository, macht etwas mit dem Code - zum Beispiel ein Build - und stellt dann die Lösung in der Cloud je nach Vernwendungszweck in geeigneter Form zur Verfügung; beispielsweise in Form einer Webanwendung.
Wichtig ist, dass ein Agent den Zustand nicht persistiert. Nach dem Deployment und dem Release des Agenten gehen alle nicht in die Cloud gesicherten Daten verloren. Ein Agent ist in diesem Sinne zustandslos.

Bei der Verwendung von Terraform hat das zur Konsequenz, dass die State-Files unbedingt in einem Storage Account abgespeichert werden müssen.

Azure Cloud Shell



Übersicht Azure DevOps


Azure DevOps ist eine webbasierte Lösung von Microsoft für die Entwicklung von DevOps Lösungen. Wer eine MSDN Subscription hat, kann auch Azure DevOps gratis nutzen:

Die Menueinträge von Azure DevOps


Die Funktionen sind vielfältig und es bedarf fast eines eigens dafür geschriebenen Buches zur Erklärung aller Funktionen. Grundsätzlich steht ganz oben links der Projektname. Das Projekt heisst in diesem Beispiel "Test". Der Menüeintrag Overview bietet eine Übersicht über das Projekt. Meistens wird auf einen Eintrag im Wiki verwiesen. Dashboards können selber zusammengestellt werden.

Beispiel von Wiki-Einträgen in einem Projekt

Unter dem Menüpunkt Boards ist es möglich, die Entwicklung mit Scrum durchzuführen:

Scrum mit Azure DevOps

Sprints

Der Menüpunkt Repos zeigt Elemente zur GIT Integration an:

GIT Integration in Azure DevOps

Ansicht der Projektstruktur mittels "Files"


Desweiteren steht noch der Menüpunkt "Test Plans" und "Artifacts" zur Verfügung, die hier aber momentan nicht von Bedeutung sind.



Erstellen des Codes im Webbrowser


Längerfristig wird der Code in Visualt Studio oder in Visual Studio Code erstellt. Für die ersten Schritte langt es aber, den Code direkt im Browser einzugeben. Dazu wird unter Repos, ein neues Repository erzeugt. Dazu wird auf "Files" geklickt:

Erstellen von einem Repository
Nachdem das Repository erzeugt wurde kann der Code direkt im Browser eingegeben und bearbeitet werden.

Folgende Verzeichnisstruktur wird erzeugt:

Verzeichnisstruktur


Die vielen "readme.md" Datei stammen daher, dass bei Erstellung eines Verzeichnisses das Verzeichnis nicht leer sein darf. Sie können dann später entferent werden. Oben im Bild ersichtlich ist, dass der Leaf "live" aus den Foldern "prod" und "staging" besteht. Vorerst wird nur der Leaf "staging" mit Werten besiedelt. Die Inhalte der obigen Dateien sind noch leer. Wie in meinem Blog "Azure Infrastruktur erstellen mit Terraform" beschrieben, wird das Verzeichnis "live" für den Infrastrukturentwickler verwendet. Codier wird unter diesem Verzeichnis nicht. Dazu dienen die Verzeichnisse "prod" und "staging" die auf gleicher Ebene wie das Verzeichnis "live" zu liegen kommen:

Die Projektstrutur für staging und prod

Die Root-Verzeichnisse "prod" und "staging" sind für den Code Entwickler vorgesehen. Wird das Verzeichnis "resource-groups" aufgeklappt, wird folgende Struktur erkenntlich:

Grundstruktur für eine von Terraform zur Verfügung gestellte Ressource

Ressourcengruppen sind ein Infrastrukturelement von Azure. Wenn die Zielsetzung besteht, alle zu erzeugenden Ressourcen nach einem logischen Muster zu erzeugen, so macht es Sinn, die Ressourcengruppen am Anfang zu erzeugen. Daraufhin können beispielsweise, die Subnetze und die virtuellen Netzwerke erstellt werden. Wird eine Infrastruktur nur benötigt, um zum Beispiel einen Webserver mit Firewall und Load-Balancer zu erstellen, kann die Projektstruktur dementsprechend abgeändert werden.

Wie gesagt leben die Eingabedaten für das Infrastrukturelement nicht im Codierungsverzeichnis sonder im Liveverzeichnis. Das Infrastrukturelement soll sich also seine Daten selber holen.



Holen der Daten aus dem Live Verzeichnis


Dazu gib es mehrere Strategien. Terraform scannt wenn es ausgeführt wird alle Dateien aus dem aktuellen Verzeichnis nach der Endung tf. Zudem erwartet es Konfigurationsdaten in einer Datei terraform.tfvars. Liegt eine solche Datei vor, wird diese automatisch eingelesen. Es gibt verschiedene Typen von Dateien - alle mit der Endung tf - die Terraform erkennt.
  • interpretierbarer Code
  • Variablen
  • Outputs
  • Backends
und Konfigurationsdateien mit der Endung tfvars.

Wie die Konfigurationsdateien in das Verzeichnis des ausführbaren Codes gelangen wird anhand Code Snippets im Artikel "Terraform Code Snippets" gezeigt.

Terraform unterstützt auch Backends. Ein Backend ist ein Speicherort für den State von Terraform.

State Dateien


Terraform wird in einem spezifischen Verzeichnis ausgeführt - dem Working Directory. Dort scannt Terraform alles Dateien mit der Endung tf und liest automatisch die Konfigurationsdatei terraform.tfvars ein, wenn vorhanden. Mit "terraform init" wird der Code überprüft auf Fehler und die notwendigen Plugins zur Ausführung werden installiert. Mit "terraform plan" wird ein Plan erstellt, welche Ressourcen erzeugt oder vernichtet werden sollen. Mit "terraform apply" werden schliesslich die Ressourcen erzeugt oder vernichtet. Dabei speichert Terraform die Ergebnisse der Operationen in einer State Datei ab. Meistens heisst sie terraform.state. Wenn neue Ressourcen hinzukommen oder Ressourcen vernichtet werden muss Terraform auf diese State Datei Zugriff haben. Diese Datei sollte also nie verloren gehen.

Wenn nun eine Pipeline benutzt wird, so läuft ja der Provisioning Vorgang auf einem zustandslosen Agenten. Die State Dateien würden also nach dem Provisioning verloren gehen. Terraform unterstützt aber Backends, dass heisst andere nicht lokale Speicherpläte für State Dateien. In Azure wird logischerweise ein Storage Account in der Cloud verwendet. Das Problem bei der Programmierung ist, dass die Definition des Backends nicht über Variablen erfolgen kann wie das normalerweise bei den Ressourcen geschieht. Die Datei backend.tf - wo das Backend definiert ist - enthielte also nur hardcodierte Einträge für den Zugriff auf einen Storage Account. Wenn mehrere Personen an einem Projekt arbeiten, hat aber jeder seinen eigenen Storage Account und damit jeder eigene Zugriffschlüssel auf das Backend.

Die Lösung ist hier ein partielles Backend zu definieren. In der Datei backend.tf wird Terraform nur gesagt, dass ein Backend verwendet werden soll. Der konkrete notwendige Inhalt für den Zugriff auf das Backend, wird dann bei der Initalisierung Terraform mitgegeben. Die notwendigen Elemente sind:
  • Der Name des Storage Accounts
  • Der Container Name
  • Der Key (der Name der Datei unter welchem der Zustand abgespeichert werden soll)
  • Ein Schlüssel für den Zugriff auf den Storage Account
Der Aufruf ist dann:



terraform init -backend-config="storage_account_name=..." 

               -backend-config="container_name=..." 
               -backend-config="key=..." 
               -backend-config="access_key=...."

In die Datei backend.tf wird nur der Eintrag

terraform {
backend "azurerm" {}
}


Dann lädt Terraform die State Datei in den Storage Account als Blob.

Wichtig ist, dass beim Zerstören der Ressourcen terraform im gleichen Verzeichnis (nur mit Definitionen des Codes) wieder zuerst initialisiert werden muss. Als nochmals 

terraform init -backend.-config="storage_account_name"..." ...


Dann holt Terraform den Stage aus der Cloud und verwendet in lokal dann bei der Erzeugung oder Vernichtung von Ressourcen.

Konfigurationsdateien


Konfigurationsdateien liegen im Verzeichnis live. Sie müssen also Terraform auch mit übergeben werden. Dies kann automatisiert werden durch einen Kopiervorgang realisiert als Terraform Code:


data "template_file" "movepath" {
   template = "../../../../live/${local.environment}/$${user}/$${project}/vars.tfvars"
   vars {
      project = "${local.prj_settings[1]}"
      user = "${local.prj_settings[3]}"
   }
   depends_on = ["data.template_file.projectpath"]
}



resource "null_resource" "cp_init" {
   provisioner "local-exec" {
     working_dir = "."
     command = "cp ${data.template_file.movepath.rendered} ./init   /terraform.tfvars"
   }
   depends_on=["data.template_file.movepath"]
}


Dazu wird ein Daten Template-File verwendet. Der Pfad auf die Konfigurationsdateien wird dort gerendert und dann mit einer Nullressource rüberkopiert ins Working-Directroy von Terraform. Am besten wird die Konfigurationsdatei im Live-Verzeichnis in die Datei terraform.tfvars kopiert. Diese wird nämlich von Terraform automatisch verarbeitet.



Zweckgebundene Verzeichnisse erstellen


Verzeichnisstruktur für Ressourcengruppen

Die Struktur aller Verzeichnisse bei der Code Definition ist in etwa so:

--- create-...
     --- init
     --- run
          --- loop

Unter dem Wurzelverzeichnis create-... ist nur der Code für das Herumkopieren der Konfigurationsdateien und der Backendinformationen in die Verzeichnisse, wo die eigentliche Ressourcenerzeugung oder Vernichtung stattfindet. Dies sind die Verzeichnisse init und das Verzeichnis loop. Init kann für die Initalisierung der Variablen verwendet werden und Run für die eigentliche Generierung der Ressourcen. Dabei steht im Vordergrund die Idee, dass immer eine Liste von Maps zur Erzeugung von Ressourcen verwendet werden soll. Eine typische Konfigurationsdatei wird also folgendermassen aussehen:

backend = {
   storage_account_name ="adsfdafdfa"
   container_name ="tstate"
   key ="terraform.tfstate"
   access_key = "..."
}


rsgs_map = [
{ name = "aname_1"
  location = "westeurope"
  tags =" key1,value1,key2,value2"
},
{ name ="aname_2"
  location = "westeurope"
  tags ="keya,valuea,keyb,valueb"
}
]


Das Problem in Terraform ist, dass keine Schleifen programmiert werden können. Aber es kann eine Ressource erzeugt werden, die n-Mal aufgerufen wird und eine Schablone für die Erzeugung einer einzelnen Ressource erzeugt. Diese Schablone wird dann in das Verzeichnis loop kopiert unter einem Namen wie beispielsweise foo-0.tf. Nach den gesamten Kopiervorgang befinden sich dort als n Dateien: foo-0.tf, foo-1.tf,...,foo-100.tf. Terraform wird dann im Code durch

resource "null_resource" "terraform-init" {

  provisioner "local-exec" {
    working_dir = "./../run/loop"
    command = "terraform init"
  }
}

resource "null_resource" "terraform-apply" {

   provisioner "local-exec" {
     working_dir = "./../run/loop"
     command = "terraform apply -auto-approve"
   }
   depends_on= ["null_resource.terraform-init"]
}


mitgeteilt, dass es sich selber im Verzeichnis loop aufrufen soll. Die Dateien foo-i.tf werden so geparst und die Ressourcen n-mal erstellt. Daher kann wenn 20-100 solcher Ressourcen erstellt werden sollen, auf einfache Weise in der Konfigurationsdatei eine Liste von Maps erstellt werden, wie oben ersichtlich ist.



Eine App ist auch ein Service Principal


Ein Service Principal ist eine Identiät, die von Automatisierungslösungen und Services verwendet wird, um sich gegenüber Azure zu authentifizieren. Die Autorisierung etwas zu tun, geschieht über die Rolle die der Service Principal zugeordnet bekommt.

Wie ein Service Principal im Azure Portal erstellt werden kann, zeigt der Blog "Azure Service Principal".

Damit Azure DevOps über einen Agenten, Ressourcen in der Azure Cloud provisionieren kann, muss zuerst ein Service Principal erstellt werden. Ist dieser Service Principal einmal erstellt, so kann in Azure DevOps eine Service Connection erstellt werden, die die Credentials des Service Principals verwendet.

Pipelines

Eine Pipeline in Azure DevOps ist eine Folge von Tasks, die in verschiedenen Jobs zusammengefasst sind, um Code in der Cloud zu deployen. Eine komplexe Pipeline kann beispielsweise für das Deployment einer Web-App Jobs umfassen, die die notwendigen Infrastrukturelemente erzeugen (Web-Server), diese konfigurieren und schliesslich die Web-App als runfähiges Modul auf dem Webserver installieren. Oftmals wird zwischen einer CI und einer CD Pipeline unterschieden. Die CI (Continous Integration) Pipeline läuft direkt auf dem im Repository vorhandenen Code. Die CI übernimmt den aktuellen Code, erstellt einen Build und führt automatisierte Tests durch. Die CI Pipeline ist dann erfolgreich, wenn der Build keine "Fehler" mehr aufweist. Die CD (Continous Delivery) Pipeline verwendet dann die von der CI erzeugten Artifakte, um die Web-App auf einem produktiven Server zu deployen.

In Azure DevOps wird deshalb zwischen Builds und Releases unterschieden. Sowohl für Builds und Releases können Pipelines erstellt werden.

Bei Terraform werden reine Infrastrukturelemente erzeugt und damit keine Artefakte. Ist der Build erfolgreich, so sind die Infrastrukturelemente nun physisch vorhanden. Daher werden Tests nicht in der produktiven Cloud sondern in einer Azure Test Cloud durchgeführt. Die Kosten gerade bei einer grossen Anzahl von Computing Ressourcen können schnell hoch werden.

Die Pipeline definieren


Unter Pipelines "Builds" auswählen

In Azure DevOps wird links das Menü Pipelines angeklickt. Im aufgeklappten Untermenü kann dann Builds angeklickt werden.

Oben in der Mitte erscheint dann der Link "+ New":

+ New Anklicken
Bei Azure DevOps muss immer zuerst angegeben werden, wo sich der Quellcode befindet, mit dem die Pipeline ein Deployment durchführen soll:


Ort der Quelldateien auswählen

Sind die Repository Daten ausgefüllt, so kann auf "Continue" geklickt werden. Im folgenden Dialog kann dann ein Template ausgewählt werden. Jedoch sucht man vergeblich nach einem Terraform Template:

Terraform Template existiert nicht, also "Empty Job" auswählen

Auf der linken Seite im nachfolgenden Dialog ist nun der Aufbau der bis hier definierten Pipeline ersichtlich. Die Pipeline holt also zuerst den Code aus einem Repository. In einem nächsten Schritt muss ein Agent Job definiert werden. Ein Job umfasst mehrere Tasks. Der Agent ist ein virtueller Computer mit einer für das Deployment geeigneten Laufzeitumgebung. Azure stellt verschiedene Pools zur Verfügung, also Konfigurationen wie der Deployment Agent aufgebaut sein soll.

Agent job 1 muss spezifiziert werden


Terraform benötigt folgenden Agent pool:

Agent pool "Hosted VS2017" auswählen

Oben bei Agent job 1 ist ein + Zeichen ersichtlich. Dies anklicken:

Nach Terraform suchen

Im Suchfeld wird Terraform eingegeben. Dann die Option "Terraform Build & Release Tasks" auswählen. Dies ist eine Erweiterung von Azure DevOps. Sie muss daher zuerst installiert werden. Ist die Installation erfolgreich, so stehen dann folgende Möglichkeiten zur Verfügung:

Die zwei Terraform Möglichkeiten

Weil ein Agent zustandslos ist, muss zuerst immer Terraform installiert werden. Dazu die Option Terraform Installer auswählen. Die Optionen für den Installer müssen nicht angepasst werden. Der Agent wird also als erstes Terraform installieren. Dann soll ein zweiter Task Terraform initialisieren und ein weiterer Task Terraform die Erzeugung der Infrastrukturelemente erlauben:

Der komplette Agent


Das Configuration Directory muss festgelegt werden

Bei Terraform init muss zudem das Backend definiert werden. Ein Agent ist ja zustandslos. Also müssen die erzeugten State-Files in der Cloud abgelegt werden. Die Backend Azure Subscription ist aber genau die Service Connection mit den Angaben des Service Principals. Sie kann über das Drop Down Menü ausgewählt. werden. Es steht auch die Option "Create Backend (if not exists)" zur Auswahl. Beim derzeitigen Stand funktioniert diese Option nicht. Jedoch kann das Backend sehr wohl definiert werden. Das Lesen - ist bei Terraform destroy wichtig - funktioniert nämlich.


Backend definieren

Damit steht die Pipeline und es können erste Versuche gemacht werden!

Wichtig: Bei der Destroy Pipeline muss auch jedesmal eine "Terraform init" Sektion angegeben werden, denn dort wird dann das State File aus der Cloud geholt.

Keine Kommentare:

Kommentar veröffentlichen