terraforming Hetzner, Pt 1

Die erste automatisierte VM

Erster Teil der terraform–Serie: Basics und eine einzelne VM.

Zum Zeitpunkt der Drucklegung benutze ich terraform in Version 1.2.0. Alles weitere installieren wir im Laufe des Wegs. :)

Vorbereitungen

Neben terraform und einem geeigneten Rechner um es auszuführen (btw, I use arch) brauchen wir einen Hetzner Cloud–Account. Die Kosten zum Experimentieren halten sich im Rahmen, ich rechne mit ca. 30–50 cent für einen Nachmittag/Abend. Wenn Du Maschinen dauerhaft laufen lassen willst kommt da natürlich mehr zusammen.

Ich vermute dass es nicht viel Aufwand ist die terraform templates auf andere Provider umzubauen — habe das aber noch nicht getestet.

Im Hetzner Cloud Account müssen wir ein Projekt anlegen, und in diesem Projekt ein API–Token, dies muss Lese– und Schreibberechtigung haben. Den Token erstmal irgendwo zwischenspeichern (wo niemand sonst drankommt).

Provider installieren und Zugang einrichten

Ich werde den Post über immer meine Quelldateien einbetten und darunter beschreiben was drin steht. Alles zusammen findest Du ganz unten als Git–Repo verlinkt.

terraform.tf:

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "1.33.2"
    }
  }
  required_version = ">= 1.1"
}

Zuerst legen wir fest das wir den hcloud–Provider nutzen werden, und setzen die Version fest. Da sich über die Zeit durchaus Parameter verändern ist nicht gegeben das die Templates so mit einer zukünftigen Providerversion funktionieren. required_version bezieht sich auf terraform selbst.

provider.tf:

provider "hcloud" {
  token   = var.hcloud_token
}

Zum Login brauchen wir das oben generiert API–Token. Das könnte hier direkt stehen (statt var...), oder jedes mal eingetippt werden, dann wäre die Konfiguraton so fertig. Ich will es aber nicht ins gitrepo einchecken, daher ists eine Variable.

variables.tf:

variable "hcloud_token" {
  sensitive = true
  # default = <defined in secret.auto.tfvars>
}

Auch hier könnte der Token als default stehen, aber auch die variables.tf will ich git haben, also...

secret.auto.tfvars:

hcloud_token = "hunter2"

Hier steht endlich der Token. tfvars–Dateien überschreiben schon gesetzte Variablen, endet der Dateiname auf .auto.tfvars werden sie automatisch geladen. Die Datei kommt ins .gitignore, und der Token bleibt sicher.

Jetzt ist das Projekt soweit das wir den Provider installieren können: terraform init

Theoretisch funktioniert jetzt auch terraform apply schon, tut aber noch nichts...

SSH Public Key und Firewall–Regeln

ssh.tf:

resource "hcloud_ssh_key" "default" {
  name       = "terraform"
  public_key = file("../ssh-terraform-hetzner.pub")
}

Upload eines SSH–Keys. In terraform wird der hier angelegte Key default heissen, in der Hetzner Console "terraform". Vergiss nicht die Datei vorher zu erzeugen oder den Pfad zu ändern ;)

firewall.tf:

resource "hcloud_firewall" "single-firewall" {
  name = "single-firewall"
  rule {
    direction = "in"
    protocol  = "icmp"
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }

  rule {
    direction = "in"
    protocol  = "tcp"
    port      = "22"
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }

  rule {
    direction = "in"
    protocol  = "tcp"
    port      = "80"
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }
}

Vorbereitung der Firewall. Eingehender Traffic wird blockiert, ausser ICMP (denn ohne icmp kein Internet), Port 22 für SSH und Port 80 für Web. Auch wenn die Beispiel–VM keinen Webserver haben wird…

Der eigentliche Server

Nun kommen wir endlich zum eigentlichen Server. Dazu gibts erstmal ein paar neue Variablen:

variables.tf:

variable "location" {
  default = "nbg1"
}

variable "server_type" {
  default = "cx11"
}

variable "os_type" {
  default = "debian-11"
}

Es wird der kleinste VM–Typ in Nürnberg, mit Debian 11. Andere gültige Werte findest Du in der Dokumentation von Hetzner.

server.tf:

resource "hcloud_server" "single-server1" {
  name        = "single-server1"
  image       = var.os_type
  server_type = var.server_type
  location    = var.location
  labels      = {
    type = "single"
  }
  ssh_keys    = [hcloud_ssh_key.default.id]
  firewall_ids = [hcloud_firewall.single-firewall.id]
  user_data   = templatefile("user-data.yaml.tpl",
    {ssh_pubkey = file("../ssh-terraform-hetzner.pub")})
}

Der eigentliche Server, heißt im terraform single-server1, in der Hetzner Console auch, einige Werte kommen aus den Variablen. Das Label wird nicht weiter benutzt und ist eigentlich egal, das wird im nächsten Artikel interessant. Wir referenzieren die vorher angelete Firewall und den SSH–Key für den root–Account. Spannend sind die letzten beiden Zeilen, damit binden wir ein Template in cloud-init ein, um den Server nach dem Boot etwas weiter einzurichten.

user-data.yaml.tpl:

#cloud-config
users:
  - name: "ansible"
    groups: ["sudo"]
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    shell: "/bin/bash"
    ssh_authorized_keys:
      - "${ssh_pubkey}"

package_update: true
package_upgrade: true

Per cloud-init legen wir einen Nutzer ansible an, der per sudo alle Befehle ausführen darf, und über die Variable ssh_pubkey aus terraform heraus den public key bekommt. Wichtig: Die erste Zeile muss dieser schusselige Kommentar sein, sonst sucht ihr eine Stunde lang warum das Skript nicht ausgeführt wird. :) Und noch ein Hinweis: Das Skript läuft los nachdem terraform schon fertig ist. Es kann also ein paar Sekunden dauern bis der ansible–Nutzer da ist.

Last but not least, output.tf:

output "test_ip" {
  description = "Test VM IP"
  value = hcloud_server.single-server1.ipv6_address
}

output "test_ipv4" {
  description = "Test VM legacy IP"
  value = hcloud_server.single-server1.ipv4_address
}

Die outputs geben einfach nur am Ende des terraform–Laufs ein paar Daten aus. Hier sehen wir bequem die IP der neu angelegten VM.

Sind alle Dateien angelegt und ausgefüllt kannst Du mit terraform plan anschauen was passieren wird, und mit terraform apply die VM erstellen. terraform destroy löscht alles wieder. Nicht vergessen, sonst läuft die Kostenuhr weiter. ;)

Die ganzen Dateien liegen auch in meinem git–Repository

Im nächsten Artikel fügen wir mehrere VMs, ein internes Netzwerk und Loadbalancer hinzu.