
HomeLab mit mehreren Clustern hinter einer IP
Homelab Übersicht
Mein kleines Kubernetes Cluster Setup sieht aktuell wie folgt aus.
graph TB; Glasfaser-->UDM-Pro; UDM-Pro-->MacMini; MacMini-->Multipass-VM1; MacMini-->Multipass-VM2; MacMini-->Multipass-VM3; MacMini-->Multipass-VM4; MacMini-->Multipass-VM5; MacMini-->Multipass-VM6; MacMini-->Multipass-VM7; MacMini-->Multipass-VM8; MacMini-->Multipass-VM9; Multipass-VM7-->HaProxy HaProxy-->K8s-Controlplane1; HaProxy-->K8s-Controlplane2; HaProxy-->K8s-Controlplane3; HaProxy-->K8s-Worker1; HaProxy-->K8s-Worker2; HaProxy-->K8s-Worker3; subgraph Cluster01; subgraph Controlplanes; Multipass-VM1-->K8s-Controlplane1; Multipass-VM2-->K8s-Controlplane2; Multipass-VM3-->K8s-Controlplane3; end; subgraph Worker; Multipass-VM4-->K8s-Worker1; Multipass-VM5-->K8s-Worker2; Multipass-VM6-->K8s-Worker3; end; end; subgraph Cluster02; Multipass-VM8-->K8sdev-Controlplane1; Multipass-VM9-->K8sdev-Worker1; end; NFS-Server-->K8s-Controlplane1; NFS-Server-->K8s-Controlplane2; NFS-Server-->K8s-Controlplane3; NFS-Server-->K8s-Worker1; NFS-Server-->K8s-Worker2; NFS-Server-->K8s-Worker3; NFS-Server-->K8sdev-Controlplane1; NFS-Server-->K8sdev-Worker1;
Alle VMs sind auf einem MacMini mit Multipass virtualisiert. Cluster01 hat 3 Controlplanes und 3 Worker. Der Cluster02 ist für DEV und Tests, hat aktuell nur ein Controlplane und einen Worker. Beide Cluster können aber bei Bedarf jederzeit vergrössert werden. Aktuell sind sogar noch 3 weitere Cluster aktiv, da aktuell viel getestet und ausprobiert wird.
Auf der Multipass VM 07 ist ein HA-Proxy installiert, der sich um die Loadbalancing Frontends und Backends kümmert. Hier ist auch je Cluster eine VIP für die Kubernetes API. Über die kann die Kubernetes API über eine IP angesprochen werden und landet dann auf eines der Controlplanes.
graph TD; subgraph API-k8s; HaProxyAPI[HAProxy, VIP: 192.168.67.200, Port 6443]-->K8s-Controlplane1; HaProxyAPI[HAProxy, VIP: 192.168.67.200, Port 6443]-->K8s-Controlplane2; HaProxyAPI[HAProxy, VIP: 192.168.67.200, Port 6443]-->K8s-Controlplane3; end;
Das gleiche gilt für die HTTP Zugriffe über Port 80, hier wird aber eine andere VIP genommen. Der Grund ist, das hier über Metallb andere oder auch weitere IPS für LoadbalancerIP oder einem weiteren IngressController im Kubernetes genutzt werden könnten. Aber auch für Migrationen des kompletten Clusters oder einzelner Services auf andere Cluster.
graph TD; subgraph Ingress-http; HaProxyhttp[HAProxy, VIP: 192.168.67.17, Port 80]-->K8s-Controlplane1-http; HaProxyhttp[HAProxy, VIP: 192.168.67.17, Port 80]-->K8s-Controlplane2-http; HaProxyhttp[HAProxy, VIP: 192.168.67.17, Port 80]-->K8s-Controlplane3-http; HaProxyhttp[HAProxy, VIP: 192.168.67.17, Port 80]-->K8s-Worker1-http; HaProxyhttp[HAProxy, VIP: 192.168.67.17, Port 80]-->K8s-Worker2-http; HaProxyhttp[HAProxy, VIP: 192.168.67.17, Port 80]-->K8s-Worker3-http; end;
Auch für https sind im HA Proxy die Controlplanes und Worker des Clusters eingetragen. Die IP 192.168.67.17 ist die gleiche, da ich hier keine Trennung vornehme. Es währe aber möglich HTTPs Traffik nur auf bestimmte Nodes zu leiten oder gar schon vorne am HA-Proxy auf eine dedizierte IP. Da kommt es immer auf das Setup und den Service an den man bereitstellen möchte. Da Option besteht aber und man könnte auf stärkere Hardware TLS laufen lassen und auf den schwachen Nodes HTTP-Only.
graph TD; subgraph Ingress-https; HaProxyhttps[HAProxy, VIP: 192.168.67.17, Port 443]-->K8s-Controlplane1-https; HaProxyhttps[HAProxy, VIP: 192.168.67.17, Port 443]-->K8s-Controlplane2-https; HaProxyhttps[HAProxy, VIP: 192.168.67.17, Port 443]-->K8s-Controlplane3-https; HaProxyhttps[HAProxy, VIP: 192.168.67.17, Port 443]-->K8s-Worker1-https; HaProxyhttps[HAProxy, VIP: 192.168.67.17, Port 443]-->K8s-Worker2-https; HaProxyhttps[HAProxy, VIP: 192.168.67.17, Port 443]-->K8s-Worker3-https; end;
VIP IPs mit Keepalived
Die VIPs werden auf der VM07 (HAProxy) per keepdalived hochgefahren.
global_defs {
enable_script_security
script_user root
}
vrrp_script chk_haproxy {
script 'killall -0 haproxy'
interval 2
}
vrrp_instance cluster01-api-vip {
interface enp0s1
state MASTER
priority 200
virtual_router_id 51
virtual_ipaddress {
192.168.64.200/24
}
}
vrrp_instance cluster01-ingress-vip {
interface enp0s1
state MASTER
priority 200
virtual_router_id 52
virtual_ipaddress {
192.168.67.17/24
}
}
Die Instancen werden einfach hinzugefügt und dabei die virtual_router_id
hochgezählt. Die müssen eindeutig sein.
Nach einem Restart von Keepalived sind nach ein paar Sekunden die zusätzlichen IPs auf dem Interface hochgefahren:
systemctl restart keepalived.service
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:73:a6:5c brd ff:ff:ff:ff:ff:ff
inet 192.168.64.92/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
valid_lft 2061sec preferred_lft 2061sec
inet 192.168.67.17/24 scope global enp0s1
valid_lft forever preferred_lft forever
inet 192.168.64.200/24 scope global secondary enp0s1
valid_lft forever preferred_lft forever
HAProxy Konfiguration
Jetzt kann der HAProxy konfiguriert werden und die einzelnen Services auf die VIPs gebunden werden.
Global und Default
Der Global und Default Block in der haproxy.cfg sieht ao aus:
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
# stats socket /run/haproxy/admin.sock mode 660 level admin
stats socket /var/run/haproxy.sock mode 600 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private
# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
Das erste Frontend
Anschliessend werden die Frontends konfiguiert. Frontends sind bei HAProxy die Services, die auf einer IP auf einem bestimmten Port listenen und auf ein Backend verweisen. Zu Backends dann weiter unten mehr.
Als erstes das Frontend für die Kubernetes API auf Port 6443.
frontend k3s-api-frontend
bind 192.168.64.200:6443
mode tcp
option tcplog
default_backend k3s-backend
Das erste Backend
Das Backend dazu enthält jetzt aber nicht nur einen Eintrag, sondern bekommt alle Controlplanes des Clusters.
backend k3s-backend
mode tcp
option tcp-check
balance roundrobin
default-server inter 10s downinter 5s
server controlplane1 192.168.64.91:6443 check
server controlplane2 192.168.64.92:6443 check
server controlplane3 192.168.64.93:6443 check
Damit sind die drei Controlplanes eingetragen und ein Aufruf auf 192.168.64.200:6443
gibt die Ausgabe der Kubernetes API zurück. Diese IP und den Port kann auch so in die Kubeconfig eingetragen werden.
Das hat den Vorteil das wir jederzeit einen oder mehrere Controlplanes offline nehmen können und weiter auf die API zugreifen können.
HTTP/HTTPS Frontend/Backend
Das Frontend für HTTP und HTTPs sieht genau so aus. Hier einmal wie es für einen Cluster sein könnte. Anschliessend aber mit weiteren Optionen, da wir ein Frontend haben wollen, welches auf mehrere Backends zeigt. Wir wollen ja extern nur eine IP benutzen. Gerade an einem Anschluss fürs HomeLab mit DSL oder Glasfaser hat man ja nur eine IP. Und IPv6 halten ja zu viele noch für Teufelswerk.
frontend k8s_http_lb_frontend
bind 192.168.64.17:80
mode http
option httplog
log-format "%ci:%cp [%t] %ft %b %s %TR/%Tw/%Tc/%Tr/%Tt %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"
default_backend k8s_http_lb_backend
frontend k8s_https_lb_frontend
bind 192.168.64.17:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
mode http
option httplog
log-format "%ci:%cp [%t] %ft %b %s %TR/%Tw/%Tc/%Tr/%Tt %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"
default_backend k8s_https_lb_backend
Und die Backends dazu:
backend k8s_http_lb_backend
mode tcp
option tcp-check
balance roundrobin
default-server inter 10s downinter 5s
server controlplane1 192.168.64.91:80 check
server controlplane2 192.168.64.92:80 check
server controlplane3 192.168.64.93:80 check
server worker1 192.168.64.94:80 check
server worker2 192.168.64.95:80 check
server worker3 192.168.64.96:80 check
backend k8s_https_lb_backend
mode http
balance roundrobin
default-server inter 10s downinter 5s
server controlplane1 192.168.64.91:443 check
server controlplane2 192.168.64.92:443 check
server controlplane3 192.168.64.93:443 check
server worker1 192.168.64.94:443 check
server worker2 192.168.64.95:443 check
server worker3 192.168.64.96:443 check
Damit wäre das ganze lauffähig und wir hätten die API, HTTP und HTTPs Ingress erreichbar.
Weitere Backends
Aber es sind ja mehrere Cluster und die externe IP am Anschluss soll ja auf den HAProxy geleitet werden und dann weiter an den entsprechenden Cluster.
Cluster01 hat die Domain k8s01.example.net und entsprechende Subdomains web1.k8s01.example.net, web2.k8s01.example.net usw.
Cluster01 DEV hat die Domain k8s01-dev.example.net und entsprechende Subdomains web1.k8s01-dev.example.net, web2.k8s01-dev.example.net usw.
Cluster02 DEV hat die Domain k8s02-dev.example.net und entsprechende Subdomains web1.k8s02-dev.example.net, web2.k8s02-dev.example.net usw.
Das ganze kann beliebig weiter getrieben werden und weitere Cluster hinzugüfgt werden.
Match Domains
Damit das ganze funktioniert müssen wir als erstes das Frontent für HTTP und HTTPs erweitern.
frontend k8s_http_lb_frontend
bind 192.168.64.17:80
mode http
option httplog
log-format "%ci:%cp [%t] %ft %b %s %TR/%Tw/%Tc/%Tr/%Tt %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"
acl is_k8s01_dev hdr(host) -m reg ^[a-zA-Z0-9-]+\.k8s01-dev\.example\.net$
acl is_k8s02_dev hdr(host) -m reg ^[a-zA-Z0-9-]+\.k8s02-dev\.example\.net$
use_backend http_k8s01_dev_backend if is_k8s01_dev
use_backend http_k8s02_dev_backend if is_k8s02_dev
default_backend k8s_http_lb_backend
frontend k8s_https_lb_frontend
bind 192.168.64.17:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
mode http
option httplog
log-format "%ci:%cp [%t] %ft %b %s %TR/%Tw/%Tc/%Tr/%Tt %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"
acl is_k8s01_dev hdr(host) -m reg ^[a-zA-Z0-9-]+\.k8s01-dev\.example\.net$
acl is_k8s01_dev req.ssl_sni -m reg ^[a-zA-Z0-9-]+\.k8s01-dev\.example\.net$
acl is_k8s02_dev hdr(host) -m reg ^[a-zA-Z0-9-]+\.k8s02-dev\.example\.net$
acl is_k8s02_dev req.ssl_sni -m reg ^[a-zA-Z0-9-]+\.k8s02-dev\.example\.net$
use_backend https_k8s01_dev_backend if is_k8s01_dev
use_backend https_k8s02_dev_backend if is_k8s02_dev
default_backend k8s_https_lb_backend
Die zusätzlichen Backends
Die beiden Backends der weiteren Cluster anlegen:
backend http_k8s01_dev_backend
mode tcp
option tcp-check
balance roundrobin
default-server inter 10s downinter 5s
server controlplane1 192.168.64.100:80 check
server worker1 192.168.64.101:80 check
backend https_k8s01_dev_backend
mode http
balance roundrobin
default-server inter 10s downinter 5s
server controlplane1 192.168.64.100:443 check
server worker1 192.168.64.101:443 check
backend http_k8s02_dev_backend
mode tcp
option tcp-check
balance roundrobin
default-server inter 10s downinter 5s
server controlplane1 192.168.64.110:80 check
server worker1 192.168.64.111:80 check
backend https_k8s02_dev_backend
mode http
balance roundrobin
default-server inter 10s downinter 5s
server controlplane1 192.168.64.110:443 check
server worker1 192.168.64.111:443 check
Damit würde das ganze jetzt mit dem Production, Dev 1 und Dev 2 funktionieren und die Requests werden je nach Domain auf den richtigen Cluster geleitet.
TLS-Termination mit Letsencrypt
Wer genau gegeuckt hat sieht in den Frontend Blöcken bei der Option bind
beim Frontent für https ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
. Der HAProxy macht die TLS-termination, also auch das handling der HTTPs Verbindungen, inklusive der Zertifikaten. Ich habe meine Domains bei Cloudflare und habe dort einen API-Key angelegt, mit dem ich DNS Zonen lesen und bearbeiten kann.
Cludflare DNS auch aus dem Grund, weil man so auch Wildcard Zertifikate erstellen kann, was die ganze Sache sehr viel einfacher macht. Man muss nicht für jeden neuen Host-Eintrag ein neues Zertifikat erstellen.
acme.sh installieren:
curl https://get.acme.sh | sh -s email=youraddress@example.com
Anscliessend noch den API-Key von Cloudflare mit dem passenden Account in die Env Vars per expose packen:
export CF_Key="763eac4f1bcebd8b5c95e9fc50d010b4"
export CF_Email="alice@example.com"
Die benötigten Zertifikate erstellen:
acme.sh --issue --dns dns_cf -d k8s01.example.net -d '*.k8s01.example.net' --server letsencrypt
acme.sh --issue --dns dns_cf -d k8s01-dev.example.net -d '*.k8s01-dev.example.net' --server letsencrypt
acme.sh --issue --dns dns_cf -d k8s02-dev.example.net -d '*.k8s02-dev.example.net' --server letsencrypt
Die Zertifikate sind damit auch erledigt und man muss diese jetzt nur noch für HaProxy bereitstellen:
cat /root/.acme.sh/k8s01.example.net_ecc/fullchain.cer /root/.acme.sh/k8s01.example.net_ecc/k8s01.example.net.key > /etc/haproxy/certs/k8s01.example.net.pem
cat /root/.acme.sh/k8s01-dev.example.net_ecc/fullchain.cer /root/.acme.sh/k8s01-dev.example.net_ecc/k8s01-dev.example.net.key > /etc/haproxy/certs/k8s01-dev.example.net.pem
cat /root/.acme.sh/k8s02-dev.example.net_ecc/fullchain.cer /root/.acme.sh/k8s02-dev.example.net_ecc/k8s02-dev.example.net.key > /etc/haproxy/certs/k8s02-dev.example.net.pem
Wenn man noch mehr Cluster und Domains hat muss man die beiden letzten Schritte auch für diese Domains ausführen.
systemctl restart haproxy.service
und prüfen ob alles ok ist. Dazu kann man auch die Stats Option im HAProxy aktivieren und auch da mit prüfen ob alle Backend eingetragen sind.
HAProxy Stats
listen stats
bind *:9000
stats enable
stats uri /stats
stats refresh 5s
stats realm HomeLab\ Statistics
stats auth youruser:yoursecretpassword
Die IP mit Port 9000 im Browser öffnen und die Stats Seite von HAProxy zeigt alle Frontends und Backends an.
Trotz TLS in HAProxy kann Cert-Manager genutzt werden
Hier wird das SSL-Offloading vom HAProxy gemacht. Das ist nötig, damit HAProxy auch in die Verbindungen gucken kann und anhad Host
im Header unterscheiden kann, zu welchem Backend der Request gesendet werden muss.
Ich teste und nutze in Kubernetes auch den Cert-Manager. Das möchte ich auch weiter nutzen. Das ist aber auch kein Problem, da ein Ingress mit TLS, über den Issuer/ClusterIssuer genau so den Cert-Manager triggert wie bis her.
Der legt dann den http-solver
an und den passenden Ingress, der über den Pfad auf den http-solver
zeigt.
Der Request geht dann ganz normal an das externe Interface, zum MacMini, HAProxy und wird dann per HTTP an den richtigen Cluster geleitet.
Sollte ich also den HAProxy für Wartungen offline nehmen müssen, dann ist für mich aktuell nur der Production Cluster relevant. Und dafür kann ich dann einfach in der UDM-Pro einfach auch direkt auf den Cluster leiten lassen. Die Zertifikate sind dort dann auch schon vorhanden und es gibt keine Probleme oder Ausfälle.
Weitere Anregungen zu HAProxy
Wer nur einen Cluster betreibt muss den SSL Part nicht machen. In dem Fall kann man auch weiter alle Zertifikate nur im Kubernetes mit dem Cert-Manager erstellen und stellt die Verbindungen im Backend, bei der Option mode
, einfach auf tcp
. Das httplog
muss dann auch entfernt werden.
Für Wartungen an Kubernetes Nodes können diese auch einfach ausgetragen werden oder hinter dem check
trägt man einfach disabled
zusätzlich ein und macht einen reload. Das setzt setzt den Server auf Maintainance.
HAProxy ist sehr mächtig. Zum Beispiel wird hier einfach nur Roundrobin genutzt. Es Loadbalancer nach roundrobin, leastconn, source IP oder weiteren Loadbalancing-Algorithmen genutzt werden. Genau so kann Sticky Sessions mit Sticky Tables genutzt werden.
Die Beispiele oben leiten bestimmte Sub-Sub-Domains auf Backends um, da auf die Subdomain gematched wird. Wer jetzt überlegt:
“Ja, aber was mache ich wenn ich alle Domains umziehen möchte, aber ich kann nicht alle auf einmal umziehen?”
Das geht auch sehr einfach und war auch am Anfang meine Überlegung es komplett so zu machen. Dann hätte ich aber jeden Hostname (Alos Sub-Sub-Domain) beim anlegen händisch im HAProxy hinzufügen müssen. Das will ich nicht, da ich einfach schnell Dinge im Kubernetes anlegen will und sie dann sofort funktionieren.
Aber man kann anstatt einem Regex auch mit Domainlisten arbeiten. Oder auch HAProxy Maps einsetzen:
Dafür eine Datei anlegen /etc/haproxy/maps/hosts.map
#domainname backendname
nginx.k8s02-dev.example.net https_k8s02_dev_backend
nginx2.k8s02-dev.example.net https_k8s02_dev_backend
nginx.k8s01-dev.example.net https_k8s01_dev_backend
ngin2.k8s01-dev.example.net https_k8s01_dev_backend
nginx.k8s01.example.net https_k8s01_backend
# [...]
api.k8s01.example.net https_k8s01_backend
Die Frontend Konfiguration dafür würde dann so ausehen:
frontend default
bind :80
use_backend %[req.hdr(host),lower,map_dom(/etc/haproxy/maps/hosts.map,be_default)]
Plus ggf. die IP fürs Bind, Loadbalancing-Algorithmen und weitere Optionen. Domain und Backend Listen können auch im Zusammenspiel mit ALCs genutzt werden. Es können auch bestimmte Routen auf andere Backend umgeleitet werden:
/etc/haproxy/maps/routes.map
/api be_api
/login be_auth
Frontend:
frontend www
bind :80
use_backend %[path,map_beg(/etc/haproxy/maps/routes.map,be_default)]
Man sieht, HAProxy ist sehr mächtig und kann sehr viel. Was heute nur noch auf HAProxy Installationen zum einsatz kommt, die man eigentlich abschalten sollte, ist die Weiche für Desktop und Mobile Webseiten. Ja das haben einige früher so gemacht und ich kenne noch ein paar die das immer noch einsetzen müssen, da deren CMS weiter betrieben werden muss, weil keiner die Eier hat auch mal alten Schrott abzuschalten. ;-)