terraforming Hetzner, Pt 2
Wed 25 May 2022Mehrere 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 :)