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"]
}



Keine Kommentare:

Kommentar veröffentlichen