terraforming Hetzner, Pt 3

Ein Single–Node kubernetes–Cluster entsteht

Die Loadbalancer, Netze uns so weiter aus dem letzten Artikel lassen wir erstmal bei Seite und bauen einen all–in–one kubernetes–Cluster auf einer VM auf.

Vorweg: Im Netz tummeln sich viele Repositories mit terraform–Plänen um kubernetes aufzubauen, oder mit Modulen um das Ganze zu kapseln. Während es mir jetzt natürlich auch nicht ausschließlich darum geht mein eigenes Rad zu erfinden verwende ich davon doch keines. Ich habe mir einige Sachen angeschaut, und bin eigentlich immer in irgendwelche Probleme gelaufen, sei es das Abhängigkeiten ohne Version angegeben waren und es mit aktuellen Providern nicht mehr lief, oder mit dem hcloud–Provider an Stellen geklemmt hat, letztlich war ich doch immer wieder dabei zu versuchen den heruntergeladenen Code zu reparieren bzw anzupassen, der (oft) doch recht komplex war. Unter "sauber aufbauen und dabei verstehen" stelle ich mir etwas anderes vor.

Am vielversprechensten sieht mir der rke–Provider von rancher aus — als vom Hersteller unterstütztes, offizielles Projekt. Mit rke (aber ohne terraform) habe ich vor einigen Jahren auch schon im Beruf Cluster aufgebaut und recht gute Erfahrungen gemacht.

Als "sanften" Start wird der erste Cluster nur einen Node haben, und auf internes Netz und Loadbalancer verzichten.

Neue Provider

terraform.tf:

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "1.33.2"
    }
    rke = {
      source  = "rancher/rke"
      version = "1.3.0"
    }
    local = {
      source  = "hashicorp/local"
      version = "2.2.3"
    }
  }
  required_version = ">= 1.1"
}

Neben dem schon erwähnten rke–Provider nutzen wir local um am Ende die kubeconfig–Datei in eine lokale Datei schreiben zu können. Nach dem Anpassen terraform init ausführen um die Provider herunterzuladen.

Neue Variablen

variables.tf:

variable "server_type" {
  default = "cx21"
}

variable "os_type" {
  default = "ubuntu-20.04"
}

variable "docker_version" {
  default = "20.10.16"
}

variable "containerd_version" {
  default = "1.6.4"
}

variable "kubernetes_version" {
  default = "v1.22.4-rancher1-1"
}

Damit wir neben dem kubernetes noch etwas Platz haben setze ich den Servertyp eine Größe hoch, ausserdem nutze ich Ubuntu — denn Debian wird von rke nicht offiziell unterstützt.

rke setzt den kubernetes–Cluster in Docker auf, daher setzen wir eine unterstützte docker– & containerd–Version.

Und zu guter letzt die aktuellste unterstützte k8s–Version.

Änderungen an Firewall und Server

firewall.tf:

resource "hcloud_firewall" "k8s-single" {
  name = "k8s-single"
  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      = "6443"
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }
}

Neben ICMP und SSH brauchen wir Port 6443 um den Cluster per kubectl zu erreichen. Wenn Du eine statische IP hast ist es natürlich umso schöner diese unter source_ips einzutragen und nicht das gesamte Internet zu erlauben. :)

server.tf:

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

  connection {
    type        = "ssh"
    user        = "root"
    host        = self.ipv6_address
    private_key = file("../ssh-terraform-hetzner")
  }

  provisioner "remote-exec" {
    inline = [
      "cloud-init status --wait"
    ]
  }
}

Der angepasste Server, cloud-init bekommt ein paar mehr Parameter, denn über das cloud–init–Skript installieren wir gleich Docker. Da wir im nächsten Schritt Docker brauchen muß terraform jetzt warten bis das Skript durchgelaufen ist. Dazu nutze ich remote-exec, das ausgeführte Kommando wartet schlicht bis der eigentlich im Hintergrund laufende Job fertig ist. Damit das Kommando ausgeführt werden kann braucht terraform noch den Zugang zum Server, dieser ist im connection–Block definiert. Steckt Dein Provider noch zu tief in der Vergangenheit fest musst du ggf. ipv4_address nutzen. :)

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

runcmd:
  - curl -LO https://download.docker.com/linux/ubuntu/dists/focal/pool/stable/amd64/containerd.io_${containerd_version}-1_amd64.deb
  - curl -LO https://download.docker.com/linux/ubuntu/dists/focal/pool/stable/amd64/docker-ce-cli_${docker_version}~3-0~ubuntu-focal_amd64.deb
  - curl -LO https://download.docker.com/linux/ubuntu/dists/focal/pool/stable/amd64/docker-ce_${docker_version}~3-0~ubuntu-focal_amd64.deb
  - dpkg -i *deb
  - usermod -a -G docker ansible

Neben dem schon bekannten User und Paketupgrade führen wir per cloud-init ein paar Kommandos aus, und zwar holen wir die fürs Ubuntu passenden Pakete, installieren sie, und ermöglichem dem Ansible–User Docker zu nutzen.

Der k8s–Cluster

cluster.tf:

resource "rke_cluster" "cluster" {
  kubernetes_version = var.kubernetes_version
  nodes {
    address = hcloud_server.k8s-single.ipv4_address
    user    = "ansible"
    role    = ["controlplane", "worker", "etcd"]
    ssh_key = "${file("../ssh-terraform-hetzner")}"
  }
  addons_include = [
    "https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.1/aio/deploy/recommended.yaml"
  ]
}

resource "local_sensitive_file" "kube_config" {
  filename = "${path.root}/kube_config_single.yml"
  content  = "${rke_cluster.cluster.kube_config_yaml}"
}

Und, endlich, der kubernetes–Cluster. Viel ist für den single node gar nicht zu tun, der erste Block setzt den Cluster in gewünschter Version auf, definiert den einen Node in allen Rollen (rke scheint IPv6 nicht zu unterstützen) und installiert kubernetes dashboard, der zweite schreibt uns die dadurch erzeugte kubeconfig in eine Datei.

Ein terraform apply später läuft also der Cluster, und lässt sich mit kubectl ansprechen:

kubectl --kubeconfig kube_config_single.yml version --short gibt die installierte Version aus.

terraform destroy nicht vergessen um die Kostenuhr anzuhalten, und alle Dateien liegen wieder im git–Repository :)

Im nächsten Artikel erweitern wir den Cluster über mehrere Nodes.