terraforming Hetzner, Pt 2

Mehrere VMs, internes Netzwerk und Loadbalancer

Nachdem wir eine erste VM automatisiert aufbauen können, erweitern wir unsere Templates ein wenig: Es werden 3 VMs mit Webservern, die in einem virtuellen internen Netzwerk stehen sollen und von einem Load Balancer angesprochen werden.

Neue Variablen

variables.tf:

variable "instance_count" {
  default = 3
}

variable "ip_range" {
  default = "10.0.30.0/24"
}

Einmal die Anzahl der zu erzeugenden VM–Instanzen, einmal ein Netzwerk–Range für das interne Netz. Hetzner baut hier leider noch voll auf legacy IP, also passt irgendwas aus den privaten Ranges aus RFC 1918.

Netzwerk und Loadbalancer

network.tf:

resource "hcloud_network" "tw_private" {
  name     = "three_web_private"
  ip_range = var.ip_range
}

resource "hcloud_network_subnet" "tw_priv_subnet" {
  network_id   = hcloud_network.tw_private.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = var.ip_range
}

resource "hcloud_server_network" "tw_network" {
  count     = var.instance_count
  server_id = hcloud_server.web-server[count.index].id
  subnet_id = hcloud_network_subnet.tw_priv_subnet.id
}

Der erste Block definiert das Netzwerk three_web_private mit der vorher ausgesuchten IP Range, der zweite richtet ein Subnet in der passenden Netzwerk–Zone ein, die Zone könnte eigentlich noch gut als Variable neben die location der Server. Der dritte Block verknüpft das Subnet mit den Servern die wir später noch definieren, über count passiert das 3 mal.

loadbalancer.tf:

resource "hcloud_load_balancer" "tw_lb" {
  name               = "three-web-load-balancer"
  load_balancer_type = "lb11"
  location           = var.location
}

resource "hcloud_load_balancer_network" "tw_lb_network" {
  load_balancer_id = hcloud_load_balancer.tw_lb.id
  subnet_id        = hcloud_network_subnet.tw_priv_subnet.id
}

resource "hcloud_load_balancer_target" "tw_lb_target" {
  type             = "label_selector"
  load_balancer_id = hcloud_load_balancer.tw_lb.id
  label_selector   = "type=web"
  use_private_ip   = true
  depends_on       = [
    hcloud_load_balancer_network.tw_lb_network
  ]
}

resource "hcloud_load_balancer_service" "lb_service" {
    load_balancer_id = hcloud_load_balancer.tw_lb.id
    protocol         = "http"
    listen_port      = 80
    destination_port = 80
}

Ich musste die Variablen etwas kürzen damit das CSS des Blogs damit klarkommt. Im Repo (ganz unten verlinkt) stehen sie ausgeschrieben :)

Wir legen hier einen Loadbalancer an, den kleinsten (lb11), in der Location in der auch unsere Server stehen werden.

Der zweite Block fügt den Loadbalancer unserem vorher definierten internen Subnet zu.

Der dritte setzt als Ziel unsere Server die wir gleich noch definieren, und zwar über das Label type=web. Wir könnten auch direkt die Server per id angeben, über die label–Config wäre es aber möglich später dynamisch VMs hinzuzufügen oder zu entfernen ohne den Loadbalancer umkonfigurieren zu müssen. use_private_ip sorgt dafür das wir die VMs über das interne Netz ansprechen.

Dadurch gibt es aber auch eine Besonderheit in diesem Block, depends_on. Um das target in der Hetzner Cloud anlegen zu können muss der Loadbalancer zu einem Subnet gehören, da in der Resource tw_lb_target aber keins angegeben würde kann terraform diese Abhängigkeit nicht erkennen und könnte die Resourcen gleichzeitig oder in falscher Reihenfolge anlegen, was zu einem Fehler führen würde. Daher sagen wir terraform über depends_on selber das erst die Netzwerkverknüpfung fertig sein muss, und erst danach das Target angelegt werden darf.

Die Servicedefinition im vierten Block schließlich setzt stumpf Port 80 am Loadbalancer auf Port 80 an den VMs um. Durch die Angabe vom protocol wird automatisch ein Health–Check eingerichtet, der prüft ob die Webserver für / antworten.

Neue Server

server.tf:

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

Gegenüber dem einzelnen Server müssen wir gar nicht viel anpassen um mehrere zu erzeugen. Neu ist count, was den Hauptteil der Magie ausmacht, der Servername ist angepasst und das Label auf web umgesetzt, damit der Loadbalancer die VMs findet. Der Rest der Serverdefinition ist unverändert.

user-data.yaml.tpl:

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

packages:
  - nginx

package_update: true
package_upgrade: true

runcmd:
  - systemctl enable --now nginx
  - echo "<h1>terraformed</h1>\nthis is $(hostname)" \
    > /var/www/html/index.html

Über cloud-init installieren wir schließlich den Webserver (nginx), starten ihn und hinterlegen eine Seite die den Hostnamen enthält.

output.tf:

output "lb_ip" {
  description = "Load balancer IP address"
  value = hcloud_load_balancer.tw_lb.ipv6
}

output "web_ips" {
  description = "Test VM IP"
  value = {
    for server in hcloud_server.web-server :
    server.name => server.ipv6_address
  }
}

output "web_ipv4" {
  description = "Test VM legacy IP"
  value = {
    for server in hcloud_server.web-server :
    server.name => server.ipv4_address
  }
}

Zuletzt das angepasste output–Template. Neu ist die Ausgabe der IP des Loadbalancers, und die IPs der Server sind um eine Schleife erweitert um alle 3 Server zu sehen.

Ausführen und Testen

Die Ausführung (terraform apply) dieses Plans dauert schon ein wenig länger als die einzelne VM, und nach dem terraform warte ich nach dem Testen noch einige Sekunden bis das cloud-init durchgelaufen und alle nginxe installiert sind.

Letztlich gibt aber terraform über die outputs aus:

lb_ip = "2a01:4f8:1c1d:aee::1"
web_ips = {
  "web-server-0" = "2a01:4f8:c0c:77d3::1"
  "web-server-1" = "2a01:4f8:1c1e:dbf4::1"
  "web-server-2" = "2a01:4f8:1c1e:e2c6::1"
}
web_ipv4 = {
  "web-server-0" = "195.201.219.117"
  "web-server-1" = "167.235.74.69"
  "web-server-2" = "195.201.98.137"
}

und über ein paar wiederholte curls sehen wir das der Loadbalancer Anfragen verteilt:

$ curl "http://[2a01:4f8:1c1d:aee::1]/"
<h1>terraformed</h1>
this is web-server-0

$ curl "http://[2a01:4f8:1c1d:aee::1]/"
<h1>terraformed</h1>
this is web-server-2

$ curl "http://[2a01:4f8:1c1d:aee::1]/"
<h1>terraformed</h1>
this is web-server-1
...

Am Schluß terraform destroy nicht vergessen, und auch diese Dateien liegen im git–Repository :)