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

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...

Terraform und Ansible - ein starkes Gespann

Einführung Terraform (IaaS - Infrastructure as a Service) und Ansible (CaaS - Configuration as a Service) bilden zusammen ein starkes Gespann, um schnell konfigurierte Ressourcen auf der Microsoft Azure Cloud zu provisionieren. In meinen bisherigen Blogartikeln habe ich bisher Terraform vorgestellt https://thomkerle.blogspot.com/2019/02/azure-infrastruktur-erstellen-mit.html https://thomkerle.blogspot.com/2019/03/azure-service-principal.html https://thomkerle.blogspot.com/2019/03/eine-komplette-terraform-pipeline-mit.html https://thomkerle.blogspot.com/2019/08/terraform-12-ist-da.html und zwar wie mit Terraform effizient Ressourcen aus Azure DevOps ( https://dev.azure.com ) provisioniert werden können. Während sich Terraform vor allem im Bereich "Ressourcen-Deployment" etabliert hat, scheint sich Ansible im Bereich "Configuration as a Service" zu etablieren. In diesem Artikel präsentiere ich ein Beispiel, wie mit Terraform Ansible Code - nach de...

Gnome Desktop auf Azure

Einleitung DaaS heisst nichts anderes als Desktop as a Service. Darunter wird die Bereitstellung eines Desktops in der Cloud verstanden. Anstatt Windows 10 zu Hause zu nutzen, wird Windows in der Cloud genutzt. Also anstatt leistungsfähige Hardware zu Hause zu horten, kann die Hardware in der Cloud genutzt werden. Mit geeigneter Software kann dann ein virtueller Desktop zur virtuellen Maschine in der Cloud lokal auf dem eigenen Rechner genutzt werden.  Im Moment ist dabei klar Citrix der Marktführer dicht gefolgt von anderen Herstellern unter anderem auch Microsoft. Schaut man in die Linux Welt, so wird mit Spice ein Protokoll angeboten, mit dem eine unter KVM laufende virtuelle Maschine angesprochen werden kann. Das Desktop Experiment übernimmt der virt-viewer, der unter unter  https://www.spice-space.org/download.html heruntergeladen werden kann. Hier gehen wir aber von einem leicht anderen Setup aus. Wir wollen den virt-viewer nutzen, um eine in Azure deployte U...