Pi-Hole auf Kubernetes
Kubernetes eignet sich hervorragend auch für kleine Setups, z.B. in einem Homelab, und auch bei begrenzten Hardwareressourcen.
Dafür gibt es beispielsweise besonders schlanke Distributionen wie k3s.
Der immense Vorteil von Kubernetes liegt in den bereitgestellten APIs und der sehr einfachen Möglichkeit, alle Konfigurationen deklarativ zu verwalten.
Um Kubernetes als gute Plattform auch für Homelab Anwendungen zu demonstrieren geht es in diesem Beitrag darum, wie der Werbeblocker Pi-Hole auf Kubernetes betrieben werden könnte.
Pi-Hole
Pi-Hole ist ein netzwerkbasierter Ansatz, um Werbung zu blockieren.
Dazu werden DNS Server und, optional, DHCP Dienste bereitgestelt.
Zur Verwaltung und Analyse gibt es auch eine Webanwendungen.
Die sich so ergebenen Ports finden sich auf der Pi-Hole Projektseite: https://docs.pi-hole.net/main/prerequisites/#ports
Praktischerweise bietet Pi-Hole direkt Container Images an, die sowohl mit Docker als auch Kubernetes genutzt werden können.
Die entsprechende Beschreibung findet sich hier: https://github.com/pi-hole/docker-pi-hole
Für Kubernetes gibt es bisher keine offizielle Beschreibung.
Die Filterkonfiguration wird in einer SQLite Datenbank gehalten, diese wird immer wieder per Download aktualisiert. Damit bietet sich die Verwendung eines Volumes für die Datenbank und auch für die später erzeugte Konfiguration von Pi-Hole an.
Pi-Hole Optionen auf Kubernetes
Es gibt nicht einen "richtigen" Weg, Pi-Hole zu betreiben. Bereits ohne Kubernetes finden sich einige dokumentierte Varianten in der offiziellen Dokumentation.
Um das Setup zunächst einfach zu halten, soll lediglich die Webanwendung und die DNS Komponente bereitgestellt werden. Damit funktioniert bereits der Werbeblocker und ist auch mit den meisten Setups kompatibel, da der neue DNS Server lediglich im vom Router (z.B. FritzBox o.ä.) bereitgestellten DHCP Server eingetragen werden muss.
Am nächsten Entscheidungspunkt fällt das Design etwas komplexer aus, da die Verwendung von Host-Ports bei Kubernetes nicht zu empfehlen ist, und zu allerlei Wechselwirkungen führen kann:
Es wird ein Service vom Typ LoadBalancer verwendet, um die Dienste im Netzwerk verfügbar zu machen.
Als schlanke Implementierungen bieten sich die Projekte kube-vip https://kube-vip.io/ und MetalLB https://metallb.universe.tf/ an.
In diesem Fall kommt MetalLB zum Einsatz und ist bereits vorkonfiguriert.
Als Implementierung für PersistentVolumes wird im Home-Setup oft auf NFS zurückgegriffen. Wenn es nur eine einzelne Node gibt, kann sogar ein festes Host-Verzeichnis genutzt werden. Um keine weitere Komplexität aufzubauen, wird für das Beispiel einfach ein emptyDir verwendet. Vorsicht: Das ist für den richtigen Einsatz nicht geeignet, bei einem Update wird dies ggf. gelöscht.
Pi-Hole selbst wird als StatefulSet betrieben, damit ist gewährleistet, dass es nicht unbeabsichtigt zu parallelen Instanzen kommt.
Umsetzung Pi-Hole mit Kubernetes
Zunächst bekommt Pi-Hole einen eigenen Namespace.
Werden keine Werkzeuge wie kustomize zur Verwaltung und Anpassung der Manifeste genutzt, hat es sich bewährt, den Manifesten über den Dateinamen eine Ordnung zu geben.
So könnte daher der Namespace durch die Datei 0-namespace.yaml
definiert werden.
apiVersion: v1
kind: Namespace
metadata:
name: pi-hole
In allen folgenden Manifesten wird der Namespace mit definiert. Dies reduziert das Fehlerpotential, jedoch ist damit kein paralleles Setup in einem anderen Kubernetes Namespace auf einfache Weise möglich.
Damit die Adminoberfläche von Pi-Hole abgesichert ist, wird ein Passwort zur Authentifizierung konfiguriert. Dies läßt sich in Kubernetes durch ein Secret umsetzen, dessen Inhalt dann als Umgebungsvariable im Pi-Hole Server Container eingebunden wird. Das ist praktischerweise durch das Pi-Hole Container Image auch direkt vorgesehen.
apiVersion: v1
kind: Secret
metadata:
name: pi-hole-admin
namespace: pi-hole
type: Opaque
stringData:
password: geheim
Nun kann der Pi-Hole Server selbst umgestzt werden.
Dazu wird ein StatefulSet verwendet, damit im Fall der Skalierung separate PersistentVolume eingebunden werden.
Für den Betrieb auf einer einzelnen Node macht das soweit keinen Unterschied.
Es ist jedoch zu beachten, dass ein StatefulSet bei einem Update bzw. Rollout stets die einzelnen Pods erst löscht und dann neu erzeugt, da die festen Podnamen auch eindeutig sein müssen.
Bei einem StatefulSet mit nur einer Instanz ist das vergleichbar mit einem Deployment und Rollout-Strategie recreate
:
Das hat die Konsequenz, dass bei einem Update Pi-Hole eine Weile nicht zur Verfügung steht, dient jedoch so als passende Vorlage, falls das Setup mit einem hostPort
verwendet werden soll.
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: pi-hole
name: pi-hole-server
spec:
replicas: 1
serviceName: pi-hole
selector:
matchLabels:
app: pi-hole
template:
metadata:
labels:
app: pi-hole
spec:
containers:
- name: server
image: pihole/pihole:latest
env:
- name: TZ
value: "Europe/Berlin"
- name: DNSMASQ_USER
value: root
- name: PIHOLE_UID
value: "0"
- name: WEBPASSWORD
valueFrom:
secretKeyRef:
name: pi-hole-admin
key: password
ports:
- name: dns-tcp
containerPort: 53
protocol: TCP
- name: dns-udp
containerPort: 53
protocol: UDP
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
livenessProbe:
httpGet:
path: /admin
port: 80
scheme: HTTP
failureThreshold: 5
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 512Mi
volumeMounts:
- mountPath: /etc/pihole
name: data
- mountPath: /etc/dnsmasq.d
name: dnsmasq
volumes:
- name: data
emptyDir: {}
- name: dnsmasq
emptyDir: {}
Um den Zugriff auf Pi-Hole zu ermöglichen wird noch ein Service benötigt.
Um das ganze einfacher nachvollziehbar zu machen werden zwei separate Services verwendet.
Die Services werden mit dem Typ LoadBalancer extern verfügbar gemacht.
Dabei sollen diese die selbe IP verwenden.
Bei der MetalLB Implementierung kann dies durch die Annotation metallb.universe.tf/allow-shared-ip
konfiguriert werden.
Weiterhin soll auch dann, wenn der Service neu angelegt wird, stets die selbe IP verwendet werden und diese auch vorab festgelegt werden.
Dies kann durch das Feld spec.loadBalancerIP
eingestellt werden, jedoch ist dies in Kubernetes deprecated worden und es wird empfohlen eine implementierungsspezifische Annotation zu verwenden.
Bei MetalLB ist dies metallb.universe.tf/loadBalancerIPs
.
(Jedoch loggt MetalLB selbst ebenfalls einen Hinweis, dass beide Annotationen deprecated sind, ein Ersatz ist jedoch bisher nicht dokumentiert.)
apiVersion: v1
kind: Service
metadata:
namespace: pi-hole
name: pi-hole
annotations:
metallb.universe.tf/allow-shared-ip: pihole-204-20
metallb.universe.tf/loadBalancerIPs: 10.23.204.20
spec:
type: LoadBalancer
#deprecated:
#loadBalancerIP: 10.23.204.20
selector:
app: pi-hole
ports:
- name: http
protocol: TCP
port: 80
targetPort: http
- name: https
protocol: TCP
port: 443
targetPort: https
Analog muss eine Konfiguration für den DNS Server Service angelegt werden. Dabei ist zu beachten, dass DNS sowohl UDP als auch TCP verwendet.
apiVersion: v1
kind: Service
metadata:
namespace: pi-hole
name: pi-hole-dns
annotations:
metallb.universe.tf/allow-shared-ip: pihole-204-20
metallb.universe.tf/loadBalancerIPs: 10.23.204.20
spec:
type: LoadBalancer
#deprecated:
#loadBalancerIP: 10.23.204.20
selector:
app: pi-hole
ports:
- name: dns-tcp
protocol: TCP
port: 53
targetPort: dns-tcp
- name: dns-udp
protocol: UDP
port: 53
targetPort: dns-udp
Die Installation erfolgt dann mit kubectl
wie bei anderen Kubernetes Manifesten.
$ kubectl apply -f .
namespace/pi-hole created
secret/pi-hole-admin created
statefulset.apps/pi-hole-server created
service/pi-hole created
service/pi-hole-dns created
Ob alles geklappt hat, läßt sich ebenfalls durch kubectl
abfragen.
$ kubectl -n pi-hole get all
NAME READY STATUS RESTARTS AGE
pod/pi-hole-server-0 1/1 Running 0 23s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/pi-hole LoadBalancer 10.96.117.2 10.23.204.20 80:32003/TCP,443:30372/TCP 23s
service/pi-hole-dns LoadBalancer 10.107.235.4 10.23.204.20 53:30866/TCP,53:30866/UDP 23s
NAME READY AGE
statefulset.apps/pi-hole-server 1/1 23s
Test von Pi-Hole auf Kubernetes
Zunächst soll das Setup getestet werden. Dazu wird die Administrationsoberfläche mit der durch den LoadBalancer bereitgestellten externen IP aufgerufen. Die URL lautet in diesem Beispielsetup http://10.23.204.20/admin
Da ein LoadBalancer zur Implementierung stets auch einen NodePort bereitstellt, kann dieser auf einer beliebigen Node des Clusters aufgerufen werden, falls die External-IP des LoadBalancers nicht funktionieren sollte.
Dann handelt es sich um ein Problem mit dem eingestzten Loadbalancer oder dem Netzwerk.
Je nach Implementierung hilft hier oft schon ein describe
auf den jeweiligen Service, hier im Beispiel mit MetalLB ist auf das Speaker-Event zu achten.
Fehlt dies, ist die Konfiguration von MetalLB nicht korrekt oder vollständig.
Weitere Informationen liefert ggf. auch die Logausgabe von Pi-Hole.
$ kubectl -n pi-hole logs statefulsets/pi-hole-server
s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
...
Wenn alles geklappt hat, kann man sich nun mit dem selbst gewählten Passwort anmelden. Über die Administrationsoberfläche können weitergehende Einstellungen vorgenommen werden, es werden aber auch Diagnoseinformationen bereitgestellt.
Als nächstes muss der DNS Server getestet werden.
Das kann beispielsweise durch das Werkzeug dig
erfolgen eine Alternative ist z.B. nslookup
.
$ dig www.tagesschau.de @10.23.204.20
; <<>> DiG 9.20.4 <<>> www.tagesschau.de @10.23.204.20
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22659
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;www.tagesschau.de. IN A
;; ANSWER SECTION:
www.tagesschau.de. 36 IN CNAME www.tagesschau.de.edgekey.net.
www.tagesschau.de.edgekey.net. 4480 IN CNAME e8178.dsce6.akamaiedge.net.
e8178.dsce6.akamaiedge.net. 20 IN A 23.37.60.179
;; Query time: 29 msec
;; SERVER: 10.23.204.20#53(10.23.204.20) (UDP)
;; WHEN: Sun Jan 26 12:01:35 CET 2025
;; MSG SIZE rcvd: 142
Jetzt kann Pi-Hole auch testweise auf einer Maschine verwendet werden.
Dazu wird die Pi-Hole IP als DNS Server eingetragen, unter Linux beispielsweise durch einen entsprechenden Eintrag in der /etc/resolv.conf
.
Nach etwas Websurfing sollten entsprechende DNS Abfragen im Pi-Hole Query zu sehen sein, wobei einige auch als blockiert zu erkennen sind.
Konfiguration des DNS Server
Damit alle Geräte im Netzwerk von Pi-Hole profitieren, muss nun der DNS Server in allen Geräten konfiguriert werden.
Der einfachste Weg ist, dies mit den DHCP Einstellungen direkt automatisiert zu verteilen.
In der Regel bieten alle Router dies über ihre Konfigurationsoberfläche an.
Werden Geräte nicht per DHCP konfiguriert, muss hier entsprechend die Konfiguration manuell erfolgen.
Fazit
Pi-Hole lässt sich komfortabel mit Kubernetes betreiben.
In diesem Beitrag wurde ein Beispielsetup als Vorlage bereitgestellt, dass sich an die jeweilige Umgebung einfach anpassen läßt.
Viel Spaß mit Pi-Hole und Kubernetes!
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.