OpenTofu¶
Mit OpenTofu kann man zum Beispiel Kubernetes-Cluster automatisiert erstellen. OpenTofu, als Open Source Branch von Terraform, bietet eine breite Unterstützung für verschiedene Cloud-Anbieter (AWS, Azure, Google Cloud, etc.) aber auch Hetzner. Die Installation von OpenTofu ist einfach, siehe https://opentofu.org/docs/intro/install/
$ curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
$ chmod +x install-opentofu.sh
$ ./install-opentofu.sh --install-method deb
$ rm -f install-opentofu.sh
$ tofu --version
In diesem Tutorrial werde ich alleine als Dozent Tofu vorführen. Ein sehr einfaches Beispiel für die Verwendung von OpenTofu ist die Erstellung der Schulungs-VMs bei Hetzner:
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "1.60.1"
}
}
}
variable "students" {
type = number
description = "Number of VMs to create"
default = 1
}
data "hcloud_ssh_key" "schulung" {
name = "schulung"
}
resource "hcloud_server" "student" {
for_each = { for vm in range(0, var.students) : vm => "student-${vm}" }
name = each.value
image = "debian-13"
server_type = "cpx32"
location = "fsn1"
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
ssh_keys = [data.hcloud_ssh_key.schulung.name]
provisioner "remote-exec" {
inline = [
"apt update",
"apt upgrade -y",
"apt install -y git",
"git clone https://github.com/trutzio/kubernetes-tutorial.git",
"apt install -y yq",
]
connection {
type = "ssh"
host = self.ipv4_address
user = "root"
private_key = file("../${data.hcloud_ssh_key.schulung.name}")
}
}
}
Diese Datei enthält die OpenTofu-Konfiguration, um eine Anzahl von VMs zu erstellen, die für die Schulung verwendet werden können. Die Anzahl der VMs kann über die Variable students angepasst werden. Jede VM wird mit Debian 13 als Betriebssystem erstellt. Als Tofu-Provider wird der Hetzner-Cloud-Provider verwendet, mit dem Abschnitt resource „hcloud_server“ „student“ wird angegeben, wie die VMs aussehen sollen und wie sie initialisiert werden sollen. Eine ausführliche Dokumentation zum Hetzner-Cloud-Provider findet man unter https://search.opentofu.org/provider/opentofu/hcloud/latest.
$ export HCLOUD_TOKEN=[your-hetzner-cloud-api-token]
$ cp schulung.* ~/kubernetes-tutorial/src/opentofu # remote
$ cd ~/kubernetes-tutorial/src/opentofu
$ chown go-r schulung
$ cd schulung-vms
$ tofu init
$ tofu plan
$ cp terraform.tfstate ~/kubernetes-tutorial/src/opentofu/schulung-vms/terraform.tfstate # remote
$ tofu plan -var students=8
$ tofu apply -var students=8
In diesem Beispiel wird der State lokal in der Datei terraform.tfstate gespeichert. Es ist jedoch auch möglich, den State remote zu speichern, zum Beispiel in einem S3-Bucket oder in einem Git-Repository. Weitere Informationen zum Thema Remote State: https://opentofu.org/docs/language/state/remote/
Mit Tofu kann man natürlich auch komplette Kubernetes-Cluster erstellen.
Single Control-Plane¶
Installiert einen einzigen k3s Control-Plane Node, der auch die Rolle eines Worker-Nodes übernimmt.
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "1.60.1"
}
}
}
data "hcloud_ssh_key" "schulung" {
name = "schulung"
}
resource "hcloud_server" "k3s-single-control-plane" {
name = "k3s-single-control-plane"
image = "debian-13"
server_type = "cx23"
location = "nbg1"
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
ssh_keys = [data.hcloud_ssh_key.schulung.name]
provisioner "remote-exec" {
inline = [
"curl -sfL https://get.k3s.io | sh -"
]
connection {
type = "ssh"
host = self.ipv4_address
user = "root"
private_key = file("../../${data.hcloud_ssh_key.schulung.name}")
}
}
}
$ cd ~/kubernetes-tutorial/src/opentofu/k3s-installation/k3s-installation-single
$ tofu init
$ tofu plan
$ tofu apply
$ tofu state list
$ tofu state show hcloud_server.k3s-single-control-plane
$ tofu state show hcloud_server.k3s-single-control-plane | grep "ipv4_address"
$ ssh -i ../../schulung root@[ip control-plane]
$ kubectl get nodes
$ exit
$ mkdir -p ~/.kube
$ scp -i ../../schulung root@[ip control-plane]:/etc/rancher/k3s/k3s.yaml ~/.kube/config
$ kubectl get nodes # error: The connection to the server 127.0.0.1:6443 was refused
$ vim ~/.kube/config # change server: https://[IP control-plane]:6443
$ kubectl get nodes
$ tofu destroy
Control-Plane mit n Worker-Nodes¶
Installiert einen k3s Cluster mit einem Control-Plane Node und n Worker-Nodes. Die Anzahl der Worker-Nodes kann über die Variable k3s_node_count angepasst werden.
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "1.60.1"
}
}
}
variable "k3s_agent_token" {
type = string
default = "secret"
}
variable "k3s_node_count" {
type = number
default = 2
}
data "hcloud_ssh_key" "schulung" {
name = "schulung"
}
resource "hcloud_server" "k3s_control_plane" {
name = "k3s-control-plane"
image = "debian-13"
server_type = "cx23"
location = "nbg1"
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
ssh_keys = [data.hcloud_ssh_key.schulung.name]
provisioner "remote-exec" {
inline = [
"curl -sfL https://get.k3s.io | K3S_AGENT_TOKEN=${var.k3s_agent_token} sh -"
]
connection {
type = "ssh"
host = self.ipv4_address
user = "root"
private_key = file("../../${data.hcloud_ssh_key.schulung.name}")
}
}
}
resource "hcloud_server" "k3s_node" {
for_each = { for node in range(1, var.k3s_node_count + 1) : node => "node-${node}" }
name = "k3s-${each.value}"
image = "debian-13"
server_type = "cx23"
location = "nbg1"
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
ssh_keys = [data.hcloud_ssh_key.schulung.name]
provisioner "remote-exec" {
inline = [
"curl -sfL https://get.k3s.io | K3S_URL=https://${hcloud_server.k3s_control_plane.ipv4_address}:6443 K3S_TOKEN=${var.k3s_agent_token} sh -"
]
connection {
type = "ssh"
host = self.ipv4_address
user = "root"
private_key = file("../../${data.hcloud_ssh_key.schulung.name}")
}
}
}
HA Kubernetes-Installation¶
Installiert einen hochverfügbaren k3s Cluster mit drei Control-Plane Nodes und n Worker-Nodes. Die Anzahl der Worker-Nodes kann über die Variable k3s_node_count angepasst werden.
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "1.60.1"
}
}
}
provider "hcloud" {
token = var.hcloud_token_schulungen
}
variable "hcloud_token_schulungen" {
sensitive = true
}
# Anzahl der nodes, kann on OpenTofu über -var k3s_node_count=n gesetzt werden
variable "k3s_node_count" {
type = number
default = 2
}
# Token für die Verbindung zwischen den control-planes
variable "k3s_server_token" {
type = string
default = "secret-master"
}
# Token für die Verbindung zwischen den agents/nodes zu dem master-0 control-plane
variable "k3s_agent_token" {
type = string
default = "secret-agents"
}
# SSH Key für die Verbindung auf die einzelnen control-planes und nodes
data "hcloud_ssh_key" "schulung" {
name = "schulung"
}
# erster, initialer master, der als control-plane dient, mit diesem master synchronisieren sich die anderen masters und nodes
resource "hcloud_server" "k3s-master-0" {
name = "k3s-master-0"
image = "debian-13"
server_type = "cx23"
location = "nbg1"
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
ssh_keys = [data.hcloud_ssh_key.schulung.name]
provisioner "remote-exec" {
inline = [
"curl -sfL https://get.k3s.io | K3S_TOKEN=${var.k3s_server_token} K3S_AGENT_TOKEN=${var.k3s_agent_token} sh -s - server --cluster-init"
]
connection {
type = "ssh"
host = self.ipv4_address
user = "root"
private_key = file("../../${data.hcloud_ssh_key.schulung.name}")
}
}
}
# weitere 2 masters, die sich mit dem master-0 initial verbinden
resource "hcloud_server" "k3s-master" {
for_each = { for server in range(1, 3) : server => "master-${server}" }
name = "k3s-${each.value}"
image = "debian-13"
server_type = "cx23"
location = "nbg1"
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
ssh_keys = [data.hcloud_ssh_key.schulung.name]
provisioner "remote-exec" {
inline = [
"curl -sfL https://get.k3s.io | K3S_TOKEN=${var.k3s_server_token} sh -s - server --server https://${hcloud_server.k3s-master-0.ipv4_address}:6443"
]
connection {
type = "ssh"
host = self.ipv4_address
user = "root"
private_key = file("../../${data.hcloud_ssh_key.schulung.name}")
}
}
}
# weitere agents/worker nodes, die sich mit master-0 initial verbinden
resource "hcloud_server" "k3s_node" {
for_each = { for node in range(1, var.k3s_node_count + 1) : node => "node-${node}" }
name = "k3s-${each.value}"
image = "debian-13"
server_type = "cx23"
location = "nbg1"
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
ssh_keys = [data.hcloud_ssh_key.schulung.name]
provisioner "remote-exec" {
inline = [
"curl -sfL https://get.k3s.io | K3S_URL=https://${hcloud_server.k3s-master-0.ipv4_address}:6443 K3S_TOKEN=${var.k3s_agent_token} sh -"
]
connection {
type = "ssh"
host = self.ipv4_address
user = "root"
private_key = file("../../${data.hcloud_ssh_key.schulung.name}")
}
}
}
Load Balancer¶
Das Zielbild einer HA-Kubernetes-Installation ist, dass die Control-Planes redundant ausgelegt sind, damit der Ausfall eines Control-Planes nicht zum Ausfall des gesamten Clusters führt. Die Mindestanzahl von Control-Planes für eine HA-Kubernetes-Installation ist drei.
Der State eines Kubernetes-Clusters mit drei Control-Planes wird in einer replizierten etcd Datenbank gespeichert.
Die Control-Planes sind über einen Load Balancer erreichbar, der die Anfragen an die Control-Planes weiterleitet. In diesem Beispiel wird HAProxy als Load Balancer verwendet, der auf einem separaten Server installiert ist.
$ cd ~/kubernetes-tutorial/src/opentofu/k3s-installation/k3s-installation-ha-load-balancer
$ tofu init
$ tofu plan
$ tofu apply
$ tofu state list
$ tofu state show hcloud_server.master-0 | grep "ipv4_address"
$ ssh -i ../../schulung root@[ip master-0]
$ kubectl get nodes
$ exit
$ tofu state show hcloud_server.load-balancer | grep "ipv4_address"
$ ssh -i ../../schulung root@[ip load-balancer]
$ systemctl status haproxy
$ ls -lah /etc/haproxy/haproxy.cfg
$ exit
$ scp -i ../../schulung haproxy.cfg root@[ip load-balancer]:/etc/haproxy/haproxy.cfg
$ ssh -i ../../schulung root@[ip load-balancer]
$ vim /etc/haproxy/haproxy.cfg # change server master-0, master-1, master-2 to the IPs of the Control-Planes
$ systemctl restart haproxy
Nun kann im Browser mit http://[ip load-balancer]/healthz überprüft werden, dass der Load Balancer healthy ist.
$ tofu state show hcloud_server.master-0 | grep "ipv4_address"
$ scp -i ../../schulung root@[ip master-0]:/etc/rancher/k3s/k3s.yaml ~/.kube/config
$ vim ~/.kube/config # change server: https://[master-0]:6443
$ kubectl get nodes
$ vim ~/.kube/config # change server: https://[master-1]:6443
$ kubectl get nodes
$ vim ~/.kube/config # change server: https://[load-balancer]:6443
$ kubectl get nodes
Bitte beachte in der Tofu-Konfiguration des HA-Kubernetes-Clusters den –tls-san ${hcloud_server.load-balancer.ipv4_address} Eintrag. Dieser Eintrag ist notwendig, damit die Kubernetes API über die IP-Adresse des Load Balancers erreichbar ist. Die IP Adresse des Load Balancers muss in den TLS-SAN Eintrag des Clusters aufgenommen werden. Ohne diesen Eintrag würde die Kubernetes API nur über die IP-Adressen der Control-Planes erreichbar sein.
$ tofu destroy
Virtuelle IP-Adresse¶
Wird über den VRRP/IP Protokoll erreicht, siehe dazu auch https://de.wikipedia.org/wiki/Virtual_Router_Redundancy_Protocol. Das Tool, das VRRP/IP implementiert ist keepalived, siehe https://www.keepalived.org/.
Ein MASTER-Load-Balancer und ein BACKUP-LB werden installiert. Der MASTER-LB übernimmt die virtuelle IP-Adresse und sendet regelmäßig Heartbeats an den BACKUP-LB. Wenn der MASTER-LB ausfällt, übernimmt der BACKUP-LB die virtuelle IP-Adresse und sorgt dafür, dass der Cluster weiterhin erreichbar ist.
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "1.60.1"
}
}
}
data "hcloud_ssh_key" "schulung" {
name = "schulung"
}
# 2 keepalived server
resource "hcloud_server" "lb" {
for_each = { for server in range(0, 2) : server => "keepalived-lb-${server}" }
name = "${each.value}"
image = "debian-13"
server_type = "cx23"
location = "nbg1"
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
ssh_keys = [data.hcloud_ssh_key.schulung.name]
provisioner "remote-exec" {
inline = [
"apt update",
"apt install -y keepalived tcpdump",
]
connection {
type = "ssh"
host = self.ipv4_address
user = "root"
private_key = file("../../${data.hcloud_ssh_key.schulung.name}")
}
}
}
$ cd ~/kubernetes-tutorial/src/opentofu/k3s-installation/k3s-keepalived
$ tofu init
$ tofu plan
$ tofu apply
$ tofu state list
$
$ tofu state show 'hcloud_server.lb["0"]' | grep "ipv4_address"
$ scp -i ../../schulung keepalived-master.conf root@[ip lb-0]:/etc/keepalived/keepalived.conf
$ ssh -i ../../schulung root@[ip lb-0]
$ systemctl status keepalived
$ vim /etc/keepalived/keepalived.conf # change [ip lb-0] and [ip lb-1]
$ systemctl restart keepalived
$ systemctl status keepalived
$ tcpdump proto 112
$
$ tofu state show 'hcloud_server.lb["1"]' | grep "ipv4_address"
$ scp -i ../../schulung keepalived-backup.conf root@[ip lb-1]:/etc/keepalived/keepalived.conf
$ ssh -i ../../schulung root@[ip lb-1]
$ systemctl status keepalived
$ vim /etc/keepalived/keepalived.conf # change [ip lb-0] and [ip lb-1]
$ systemctl restart keepalived
$ systemctl status keepalived
$ tcpdump proto 112
$
$ # lb-0
$ ip -4 addr show eth0
$
$ # lb-1
$ ip -4 addr show eth0
$
$ # lb-0
$ systemctl stop keepalived
$
$ # lb-1
$ ip -4 addr show eth0
Unterschieden werden muss noch der Fall, ob der komplette LB als Server ausfällt oder nur der Keepalived-Dienst oder nur der haproxy-Dienst.