Dual-Stack mit IPv6 für Talos Kubernetes auf Hetzner Cloud
Im Beitrag Talos Kubernetes auf Hetzner Cloud einfach einrichten wurde ein Kubernetes Cluster mit Talos Linux auf einem Hetzner Cloud Server eingerichtet.
Die dort erzeugte Netzwerkkonfiguration deckte bishher lediglich das klassische IPv4 ab.
Inzwischen sind IPv4 Adressen ein knappes Gut, so dass diese auch zunehmend teurer werden.
Hetzner stellt jedem Cloud Server zusätzlich ein /64 IPv6-Subnetz bereit, das ohne Aufpreis genutzt werden kann.
Dieser Beitrag erweitert das Vorgehen und somit auch das Script, das den Netzwerk-Patch erzeugt, um IPv6. Anschließend werden die wesentlichen Unterschiede beschrieben, die beim Betrieb von Talos und Kubernetes im Dual-Stack, also mit IPv4 und IPv6 parallel, zu beachten sind.
Beim Dual-Stack sind zwei Ebenen zu unterscheiden, die unabhängig voneinander konfiguriert werden.
Die erste Ebene ist das Node-Netzwerk, also die Betriebssystemebene:
Die Talos Node erhält neben ihrer IPv4-Adresse eine IPv6-Adresse und eine IPv6-Default-Route.
Die zweite Ebene ist das Kubernetes Cluster-Netzwerk (Overlaynetwork / CNI): Pods und Services bekommen zusätzlich Adressen aus IPv6-Bereichen.
Die erste Ebene wird über einen Patch der Talos Netzwerkkonfiguration abgedeckt und ist unkompliziert, die zweite Ebene erfordert weitere Konfiguration und die Wahl eines geeigneten CNI.
IPv6 auf Hetzner Cloud
Hetzner weist jedem Cloud Server ein /64 IPv6-Subnetz zu, zum Beispiel 2a01:4f9:c014:f89::/64.
Aus diesem Subnetz kann eine beliebige Adresse für den Server verwendet werden, üblich ist die erste Adresse, also 2a01:4f9:c014:f89::1.
Das Default-Gateway ist bei Hetzner die Link-Local-Adresse fe80::1.
Eine Link-Local-Adresse ist nur im Kontext einer bestimmten Netzwerkschnittstelle eindeutig.
Die IPv6-Default-Route muss deshalb der Schnittstelle zugeordnet sein, über die fe80::1 erreichbar ist.
In der Talos-Konfiguration ergibt sich das automatisch, weil die Route unterhalb des Interface-Eintrags steht.
Die IPv6-Adresse ist in der Boot-Phase noch nicht relevant.
boot-to-talos konfiguriert über den Kernel-Parameter ip= nur IPv4, damit die Node im Maintenance-Modus per Talos API erreichbar ist.
IPv6 wird erst mit der Maschinenkonfiguration ergänzt, also mit demselben Patch, der auch die IPv4-Adresse fest zuordnet.
Netzwerk-Patch für Dual-Stack erweitern
Das Script aus dem vorherigen Beitrag ermittelt Schnittstelle, IPv4-Adresse und Gateway und schreibt daraus einen Patch. Für Dual-Stack werden zusätzlich die globale IPv6-Adresse und das IPv6-Default-Gateway ausgelesen und, sofern vorhanden, ergänzt. Wird auf dem System keine globale IPv6-Adresse gefunden, bleibt der Patch reines IPv4, das Script funktioniert also unverändert auch ohne IPv6.
boot-to-talos läuft in der bestehenden Linux-Umgebung, in der die Netzwerkkarte als eth0 erscheint.
Talos vergibt nach der Installation jedoch eigene, vorhersagbare, Interface-Namen, sodass ein fest eingetragenes interface: eth0 nach dem Neustart nicht mehr passt.
Der Patch wählt die Schnittstelle deshalb nicht über den Namen, sondern über einen deviceSelector anhand der MAC-Adresse (hardwareAddr).
Die MAC-Adresse bleibt beim Wechsel von Linux zu Talos gleich, sodass die Konfiguration unabhängig vom später vergebenen Namen greift.
Eine weitere Hetzner-Besonderheit betrifft IPv4: Der Server erhält eine /32-Adresse, das Gateway 172.31.1.1 liegt damit außerhalb des eigenen Subnetzes.
Der Patch ergänzt deshalb eine gateway-lose Hostroute auf 172.31.1.1/32, die als "scope link" direkt über die Schnittstelle aufgelöst wird und das Gateway erreichbar macht.
Für IPv6 ist das nicht nötig, weil das Link-Local-Gateway fe80::1 ohnehin direkt über die Schnittstelle erreichbar ist.
#!/bin/bash
set -euo pipefail
PATCH_FILE="${1:-$(hostname).patch.yaml}"
HOSTNAME=$(hostname)
IFACE=$(ip -4 route show default | awk '{print $5}')
MAC=$(cat "/sys/class/net/$IFACE/address")
DISK=$(lsblk -ndo NAME,TYPE | awk '$2=="disk"{print "/dev/"$1; exit}')
# IPv4-Adresse und Gateway
CIDR4=$(ip -4 addr show "$IFACE" | awk '/inet / {print $2}' | head -1)
GW4=$(ip -4 route show default | awk '{print $3; exit}')
# IPv6: globale Adresse und Default-Gateway, sofern vorhanden
CIDR6=$(ip -6 addr show "$IFACE" scope global | awk '/inet6/ {print $2}' | head -1)
GW6=$(ip -6 route show default | awk '{print $3; exit}')
# Adress- und Routenblock beginnen mit IPv4
ADDRESSES=" - ${CIDR4}"
ROUTES=" - network: ${GW4}/32 # Hostroute zum Gateway (scope link), Hetzner-spezifisch
- network: 0.0.0.0/0
gateway: ${GW4}"
VALID_SUBNETS=" - ${CIDR4%/*}/32"
# IPv6 nur ergänzen, wenn Adresse und Gateway erkannt wurden
if [ -n "$CIDR6" ] && [ -n "$GW6" ]; then
ADDRESSES="${ADDRESSES}
- ${CIDR6}"
ROUTES="${ROUTES}
- network: \"::/0\"
gateway: \"${GW6}\""
VALID_SUBNETS="${VALID_SUBNETS}
- ${CIDR6}"
fi
cat > "$PATCH_FILE" <<EOF
machine:
network:
interfaces:
- deviceSelector:
hardwareAddr: "${MAC}"
dhcp: false
addresses:
${ADDRESSES}
routes:
${ROUTES}
nameservers:
- 9.9.9.9
- 2620:fe::fe
kubelet:
nodeIP:
validSubnets:
${VALID_SUBNETS}
install:
disk: ${DISK}
---
apiVersion: v1alpha1
kind: HostnameConfig
hostname: ${HOSTNAME}
EOF
cat "$PATCH_FILE"
Der so erzeugte Patch enthält nun, falls unterstützt, beide Adressfamilien:
machine:
network:
interfaces:
- deviceSelector:
hardwareAddr: "92:00:08:cf:da:b4"
dhcp: false
addresses:
- 65.21.152.105/32
- 2a01:4f9:c014:f89::1/64
routes:
- network: 172.31.1.1/32 # Hostroute zum Gateway (scope link), Hetzner-spezifisch
- network: 0.0.0.0/0
gateway: 172.31.1.1
- network: "::/0"
gateway: "fe80::1"
nameservers:
- 9.9.9.9
- 2620:fe::fe
kubelet:
nodeIP:
validSubnets:
- 65.21.152.105/32
- 2a01:4f9:c014:f89::1/64
install:
disk: /dev/sda
---
apiVersion: v1alpha1
kind: HostnameConfig
hostname: ubuntu-4gb-hel1-1
Angewandt wird der Patch wie bisher zusammen mit der Maschinenkonfiguration:
$ talosctl apply-config --insecure \
-n $IP \
--file controlplane.yaml \
--config-patch @patch.yaml
Unterschiede bei Talos
Auf Ebene der Node fallen die Unterschiede zwischen Single-Stack und Dual-Stack überschaubar aus.
Zwei Adressfamilien pro Interface:
Unter addresses stehen sowohl die IPv4- als auch die IPv6-Adresse.
Die IPv4-Adresse behält bei Hetzner die Netzmaske /32, die IPv6-Adresse erhält /64.
Zwei Default-Routen:
Neben der Route für 0.0.0.0/0 kommt eine Route für ::/0 hinzu.
Das IPv6-Gateway fe80::1 ist eine Link-Local-Adresse und deshalb nur über die zugehörige Schnittstelle nutzbar.
Kubelet-Node-IP:
Über machine.kubelet.nodeIP.validSubnets wird festgelegt, aus welchen Subnetzen die Node ihre Adressen für die Registrierung in Kubernetes wählt.
Mit einem Eintrag je Adressfamilie registriert sich die Node mit einer IPv4- und einer IPv6-InternalIP, was für den Dual-Stack-Betrieb notwendig ist.
Ohne diese Angabe wählt das Kubelet in der Regel nur eine Adresse aus.
DNS:
Ein zusätzlicher IPv6-Resolver ist nicht zwingend, schadet aber nicht.
Im Beispiel wird neben 9.9.9.9 der IPv6-Resolver 2620:fe::fe eingetragen.
Talos API und Endpoint:
Soll die Talos API oder der Kubernetes API-Server auch über IPv6 erreichbar sein, gehört die IPv6-Adresse zusätzlich in machine.certSANs.
Unterschiede bei Kubernetes
Deutlich mehr ist auf Cluster-Ebene zu beachten, wenn auch Pods und Services IPv6 erhalten sollen.
Pod- und Service-Subnetze:
podSubnets und serviceSubnets bekommen je einen zusätzlichen IPv6-Bereich.
Diese Werte werden beim Bootstrap des Clusters festgelegt und lassen sich später nicht ohne Neuaufbau ändern.
Für die Pods und Services werden private ULA-Bereiche (fd00::/8) verwendet, keine öffentlichen Adressen.
cluster:
network:
podSubnets:
- 10.244.0.0/16
- fd00:10:244::/56
serviceSubnets:
- 10.96.0.0/12
- fd00:10:96::/112
Reihenfolge bestimmt die primäre Adressfamilie:
Der jeweils erste Eintrag legt fest, welche Familie ein Service standardmäßig als primäre ClusterIP erhält.
Steht IPv4 vorne, bleiben Services zunächst IPv4-primär, IPv6 kommt nur bei explizit dual konfigurierten Services hinzu.
Das IPv6-Service-Netz darf nicht größer als /108 sein, ein /112 wie im Beispiel ist üblich.
Adressvergabe pro Node:
Der kube-controller-manager schneidet aus dem Pod-Subnetz je Node ein kleineres Netz heraus.
Für IPv6 ist die Standardgröße /64, ein Pod-Subnetz von /56 reicht damit für 256 Nodes.
Bei dual-stack muss die Node sowohl ein IPv4- als auch ein IPv6-podCIDR zugewiesen bekommen, sonst startet das CNI für die fehlende Familie nicht.
CNI:
Das in Talos vorkonfigurierte Flannel deckt IPv4 ab, die IPv6-Unterstützung ist jedoch begrenzt.
Für vollständiges Dual-Stack-Pod-Networking ist ein CNI mit ausgereifter IPv6-Unterstützung eine bessere Wahl.
Für IPv6-Routing zwischen den Nodes muss zudem Forwarding aktiviert sein, was sich über den Sysctl net.ipv6.conf.all.forwarding sicherstellen lässt.
Egress ins IPv6-Internet:
Die Pods verwenden ULA-Adressen, die nicht global geroutet werden.
Für ausgehende IPv6-Verbindungen aus Pods ist deshalb entweder Masquerading (NAT) auf die öffentliche IPv6-Adresse der Node oder ein je Node geroutetes globales Präfix nötig.
Der NAT Modus würde dabei dem typischen Verhalten von Kubernetes unter IPv4 entsprechen.
Sollen die Pods eigene Adressen nutzen, ist das eine bewusst zu treffende Architekturentscheidung und sollte gut überlegt sein.
Services:
Ein Service steht standardmäßig als Single-Stack in der primären Adressfamilie zur Verfügung.
Für eine zusätzliche IPv6-ClusterIP wird ipFamilyPolicy auf PreferDualStack oder RequireDualStack gesetzt und ipFamilies entsprechend angegeben.
apiVersion: v1
kind: Service
metadata:
name: echo
spec:
ipFamilyPolicy: PreferDualStack
ipFamilies:
- IPv4
- IPv6
selector:
run: echo
ports:
- port: 3000
targetPort: 3000
Validierung
Ist die Node mit beiden Adressfamilien registriert, zeigt kubectl get nodes -o wide neben der IPv4- auch die IPv6-Adresse als InternalIP.
Ein Service mit ipFamilyPolicy: PreferDualStack erhält zwei ClusterIP-Adressen, eine je Familie.
$ kubectl get svc echo
NAME TYPE CLUSTER-IP ...
echo ClusterIP 10.96.0.42 ...
$ kubectl get svc echo -o jsonpath='{.spec.clusterIPs}'
["10.96.0.42","fd00:10:96::2a"]
Fazit
Auf Node-Ebene ist Dual-Stack mit Talos auf Hetzner Cloud unkompliziert:
Der erweiterte Netzwerk-Patch ergänzt IPv6-Adresse, IPv6-Route und die passende Kubelet-Konfiguration automatisch.
Auf Cluster-Ebene ist mehr zu beachten, insbesondere die zusätzlichen Pod- und Service-Subnetze, die Wahl des CNI und die Frage, wie IPv6-Egress aus Pods geregelt wird.
Wer zunächst nur eine über IPv6 erreichbare Node benötigt, kommt mit dem erweiterten Patch bereits ans Ziel, ohne das Cluster-Netzwerk umzustellen.
Zu den Themen Kubernetes, Docker und Cloudarchitektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.