Direkt zum Hauptbereich

Terraform 12 ist da



Terraform ist ein Provisioning Tool für Infrastrukturen in der Cloud. Terraform bietet für verschiedenste Cloud-Platformen (Google Cloud, Amanzon AWS, Microsoft Azure) ein Provider-Interface, das die meisten zu automatisierenden Aufgaben abdeckt. Dieser Artikel bezieht sich auf die Microsoft Azure Cloud. In meinem Artikel Azure Infrastruktur erstellen mit Terraform habe ich beschrieben wie mit Terraform 11 (aktuellste Verion 11.14) eine Infrastruktur unter Azure aufgebaut werden kann. Terraform 11 offenbart insgesamt von der Gesamtbeurteilung her, in Hinsicht auf das Provisioning, einige Mängel, die oftmals nur mit einem Hack zu beheben sind. So fällt auf, dass Kontrollstrukturen wie FOR und IF-THEN-ELSE in Terraform 11 komplett fehlen. Es gibt zwar die reservierte Variable count mit der Elemente mehrmals - Elemente sind im Azure Contex Ressourcen - erzeugt werden können. Beispielsweise aber für Module existiert dieses reservierte Wort nicht und ein Modul ist daher für die gleichzeitige Erzeugung von Ressourcen nur bedingt geeignet. Mit Terraform 11 müssen auch Abstriche bezüglich dynamische Konfiguration von Ressourcen gemacht werden, weil Maps nicht verschachtelt dargestellt werden können. Auch die Verwendung von JSON für die Zusammenarbeit mit anderen Tools ist nur beschränkt möglich. Zwar können Elemente in JSON codiert werden aber nicht decodiert.


Einleitung

Mit Terraform 12 (Stand 02.08.2019: 12.05) wurde die HCL (Hashicorp Configuration Language) in einiger Hinsicht überarbeitet:
  • Innerhalb der Ressourcendefinition ist es nun möglich FOR-Loops zu verwenden. Diese Loops erlauben dynamisch in Inline-Blocks mehrere Subressourcen zu erzeugen. Subressourcen sind beispielsweise an eine Virtuelle Maschine angehängte Festplatten. Anstatt für jede anzuhängende Festplatte einen Inline Block zu definieren, kann der Inline Block dynamisch gestaltet werden und über die Anzahl anzuhängender Festplatten iteriert werden.
  • Maps können nun verschachtelt werden. In Terraform 11 war es zwar auch möglich eine Map zu definieren, aber nur auf einer Ebene. Sub-Maps, Maps die in einer Map eingekapselt sind, waren nicht möglich. Diese Neuerung ist auf der Ebene der Konfiguration wesentlich, denn Strukturen können nun viel besser auf die zu erzeugenden Ressourcen abgebildet werden. 
  • Dies ist vor allem der JSON Unterstzützung zu verdanken. Konfigurationen können nun vollständig in JSON formuliert werden. 
  • Die Definition von sogenannten herdoc Strings ermöglicht nun auch die Defintion von Strings über mehrere Zeilen hinweg.
  • Vereinfachter Zugriff auf Variablen
Eine vollständige Liste aller Neuerungen findet sich unter: https://www.terraform.io/docs/configuration/index.html

Verwendung dynamischer Blöcke

Die Verwendung dynamischer Blöcke hat den Vorteil - obschon die Terraform Dokumentation davon abrät sie extensiv zu nutzen aus Gründen der Nachvollziehbarkeit von Code -, dass eine zu erzeugende Ressource nicht mehrmals definiert werden muss, wenn Inline-Blocks verwendet werden, die mehrmals auftauchen können. In Terraform 11 musste auf der Ressourcendefinition meist eine Bedingung an die reservierte Count-Variable gemacht werden und die Ressource mehrmals definiert werden, damit dynamisch Inline Blocks verwendet werden können.

Das Vorgehen sah also folgendermassen aus:


resource "azurerm_virtual_machine" "main" {
   count = "${var.vm_map["data_disks_count"]=="1" ? 1 : 0 }"

   name = "VM-Test"
   ...
   storage_data_disk {
      name = "VM-Test-datadisk01"
      ...
   }
}

resource "azurerm_virtual_machine" "main" {
   count = "${var.vm_map["data_disks_count"]=="2" ? 1 : 0 }"
   # Diese Ressource nur erzeugen, wenn zwei Datendisks erzeugt werden sollen.

   name = "VM-Test"
   ...
   storage_data_disk {
      name = "VM-Test-datadisk01"
      ...
   }
   # hier eine zweite Datendisks zum Anfügen
   storage_data_disk {
      name = "VM-Test-datadisk02"
      ...
   }
   ...
}

In Terraform 12 geht es eben nun einfacher. Die Datendisks können dynamisch angehängt werden. Beispielsweise sei folgende Konfigurationsstruktur für eine virtuelle Maschine vorhanden:


 {
     vm_name = "MYVM"
     vm_location = "westeurope"
     vm_resource_group_name = "test"
     vm_size = "Standard_DS1_v2"
     
     storage_image_publisher = "Canonical"
     storage_image_offer = "UbuntuServer"
     storage_image_sku = "16.04-LTS"
     storage_image_version = "latest"

     storage_disk_name = "MYVM-osdisk"
     storage_disk_caching = "ReadWrite"
     sotrage_disk_create_option = "FromImage"
     storage_disk_managed_disk_typ = "Standard_LRS"

     os_profile_computer_name = "MYVM"
     os_profile_admin_user_name ="testadmin"
     os_profile_admin_password = "Password!1234"

     vm_tags = {
            "key1" = "value1"
            "key2" = "value2"
     }

     datadisks = [
         {
             datadisk_name ="MYVM-datadisk1"
             datadisk_caching = "ReadWrite"
             datadisk_create_option = "Empty"
             datadisk_disk_size_gb = "20"
             datadisk_lun = "0"
             datadisk_managed_disk_type ="Standard_LRS"
         },
         {
             datadisk_name ="MYVM-datadisk2"
             datadisk_caching = "ReadWrite"
             datadisk_create_option = "Empty"
             datadisk_disk_size_gb = "15"
             datadisk_lun = "1"
             datadisk_managed_disk_type ="Standard_LRS"
         }
     ]
    # Definition of attachements
    vm_virtual_network_name = "MyVnet"
    vm_virtual_network_resource_group_name ="test"
    vm_subnet_name ="internal"
    vm_subnet_resource_group_name = "test"
    vm_network_interface_name ="MYVM-NIC"
    vm_network_interface_resource_group_name = "test"
    vm_ip_configuration_name ="MYVM-ipConf"

 }

Wie oben ersichtlich ist, gelingt es mit der Definition von Sub-Maps eine Ressource fast identisch abzubilden. Die Ressource, die dann die virtuelle Maschine erzeugt ist wie folgt definiert:



resource "azurerm_virtual_machine" "main" {
  name                  = var.vm_def.vm_name
  location              = var.vm_def.vm_location
  resource_group_name   = var.vm_def.vm_resource_group_name
  network_interface_ids = ["${azurerm_network_interface.main.id}"]
  vm_size               = var.vm_def.vm_size

  # Uncomment this line to delete the OS disk automatically when deleting the VM
  # delete_os_disk_on_termination = true


  # Uncomment this line to delete the data disks automatically when deleting the VM
  # delete_data_disks_on_termination = true


  storage_image_reference {
    publisher = var.vm_def.storage_image_publisher
    offer     = var.vm_def.storage_image_offer
    sku       = var.vm_def.storage_image_sku
    version   = var.vm_def.storage_image_version
  }

  storage_os_disk {
    name              = var.vm_def.storage_disk_name
    caching           = var.vm_def.storage_disk_caching
    create_option     = var.vm_def.sotrage_disk_create_option
    managed_disk_type = var.vm_def.storage_disk_managed_disk_typ
  }
  
  os_profile {
    computer_name  = var.vm_def.os_profile_computer_name
    admin_username = var.vm_def.os_profile_admin_user_name
    admin_password = var.vm_def.os_profile_admin_password
  }

  os_profile_linux_config {
    disable_password_authentication = false
  }

  dynamic "storage_data_disk" {
    for_each = [for datadisk in var.vm_def.datadisks: {
         name = datadisk.datadisk_name
         caching = datadisk.datadisk_caching
         create_option = datadisk.datadisk_create_option
         disk_size_gb = datadisk.datadisk_disk_size_gb
         lun = datadisk.datadisk_lun
         # Optional 
         # write_accelerator_enabled = datadisk.datadisk_write_accelerator_enabled
         managed_disk_type = datadisk.datadisk_managed_disk_type 
    }]

    content {
         name = storage_data_disk.value.name
         caching = storage_data_disk.value.caching
         create_option = storage_data_disk.value.create_option
         disk_size_gb = storage_data_disk.value.disk_size_gb
         lun = storage_data_disk.value.lun
         # Optional 
         # write_accelerator_enabled = storage_data_disk.datadisk_write_accelerator_enabled
         managed_disk_type = storage_data_disk.value.managed_disk_type
    }
    
  }

  tags = var.vm_def.vm_tags
  depends_on = [azurerm_network_interface.main]
}

Mehrfaches Erzeugen von Ressourcen

Noch immer besteht ein Problem bei der Erzeugung identischer Ressourcen mit verschiedenen Konfigurationselementen. Eine Liste von Maps kann also immer noch nicht ohne Umwege direkt in Terraform gebraucht werden. Das ist unschön. Den beispielsweise für die Erzeugung von über 100 Firewall-Rules wäre es doch angenehm, wenn dies realisiert werden könnte. Folgendes Konstrukt funktioniert also nicht:


resource "azurerm_resource_group" "test" {
  count = length (var.resource_groups)
  
  for_each = var.resource_groups[count.index]
  
  name     = each.value["name"]
  location = each.value["location"]
}

Noch immer ist die Idee, wie im obigen Blogartikel erwähnt, Terraform iterativ anzuwenden. Dann könnnen FOR-Loops für ganze Ressourcen erzeugt werden.


resource "null_resource" "splitter" {
  
  count = length(var.vm_defs)

  triggers = {
      test =  jsonencode(var.vm_defs[count.index])
  }
}


data "template_file" "vm" {
   count = length(var.vm_defs)

   template  = "$${definition}"
   vars = {
      test =  "jsonencode(var.vm_definitionsmap[count.index])"
      definition = <<EOT
          module "create-vm-${count.index}" {
              source = "../vm"
              vm_def = local.vm_definitionsmap_${count.index}
          }

          locals {
            vm_definitionsmap_${count.index} = ${null_resource.splitter[count.index].triggers.test}
          }
     EOT 
  }
  depends_on = ["null_resource.splitter"]
}


resource "local_file" "foo" {
    count = length(var.vm_defs)

    content     = "${element(data.template_file.vm.*.rendered, count.index)}"
    filename = "./loop/foo-${count.index}.tf"
    depends_on = ["data.template_file.vm"]
}

resource "null_resource" "terraform-init" {
    provisioner "local-exec" {
      working_dir = "./loop"
      command = "terraform init"
    }
    
    depends_on = ["local_file.foo"]
}

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



Kommentare

Beliebte Posts aus diesem Blog

Azure Service Principal

  Azure Service principal Ein Azure Service Principal ist ist eine im Azure Portal definierte Identität, die von Automatisierungstools und Services gebraucht wird, um auf spezifische Ressourcen innerhalb der Azure Cloud zuzugreifen. Die Identität verfügt über ein Login mit Passwort oder aber über ein Zertifikat, um sich gegenüber Azure zu authentifizieren. Da die Identität über eine spezfische Rolle verfügt kann die Autorisierung gegenüber ausführbarer Funktionen fein granular gesteuert werden. Mittels der Identität kann dann beispielsweise eine in Azure DevOps entwickelte App, automatisch auf ein Server in der Azure Cloud deployt werden. Service connection In Azure DevOps muss eine Service Connection erstellt werden, so dass Azure DevOps mit den Angaben des Service Prinicpals automatisch eine Anwendung deployen kann. Das Vorgehen für die Erstellung eines Service Principals ist wie folgt: Projects auswählen Zuerst wird in den "Organization Settings" der...

Python : Einführung in pygame

Einführung Pygame ist eine Bibliothek in Python, die es erlaubt grafisch anspruchsvolle Spiele zu erstellen. Damit pygame effektiv eingesetzt werden kann, braucht es doch einiges an Basiswissen, das in diesem Blog vermittelt wird. Gerade für Spiele wird vielmals von Elementen der Künstlichen Intelligenz Gebrauch gemacht. Teilweise handelt es sich nur um offensichtliche KI-Routinen aber andererseits auch um hochkomplexe Algorithmen, die nicht so trivial zu verstehen sind. Vor etlichen Jahren habe ich ein Buch gekauft, mit dem ansprechenden Namen " AI for Game developers " von David M. Bourg und Glenn Seemann. Das Ziel war es, herauszufinden, wie einfach ein Spiel unter Java 5 realisiert werden könnte. Das ganze sollte ein Kundenprojekt werden, ist aber dann nie zustande gekommen, weil der Aufwand zu gross war, das Projekt mit den bestehenden Java Bibliotheken umzusetzen. Für meine weitere "Forschung" hinsichtlich KI habe ich dieses Buch wieder zur Hand genomm...

Python: Einführung in pygame - Intelligenter Gegenspieler

Einführung In meinem ersten Blog zum Thema "Pygame" - Python: Eine Einführung in pygame - habe ich kurz erklärt wie Animationen mittels pygame realisiert werden können. Auch ein fundamentales Thema, nämlich die Kollisionserkennung wurde kurz behandelt. In diesem zweiten Blog, befassen wir uns intensiv mit dem Thema "Chasing and Evading", was übersetzt heisst: Jagen und Ausweichen. Dabei geht es um zwei Objekte, die sich animiert über das Surface (Windows-Oberfläche des Spiels) bewegen. Die zwei Objekte heissen im Spielekontext Prey (Opfer/Beute) und Predator (Raubtier/Plünderer). Dabei ist der computer kontrollierte Spieler meistens der Predator und der Prey, der vom Gamer kontrollierte Spieler.  Chasing and Evading wird schon in den Kontext von Künstlicher Intelligenz gestellt, obschon im Hintergrund kein grosses "neuronales Netz" oder irgendein selbst lernender Algorithmus steckt. Die Verfolgung durch den Computer basiert eigentlich auf einem C...