Neuigkeiten von trion.
Immer gut informiert.

Pi-Hole auf Kubernetes

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.

Namespace für Pi-Hole
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.

Secret für Administrationskennwort
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.

StatefulSet für Pi-Hole
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.)

Service für Pi-Hole Webanwendung
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.

Service für Pi-Hole DNS Server
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.

Installation Pi-Hole Manifeste mit kubectl
$ 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.

Diagnose Pi-Hole in Kubernetes
$ 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

Pi-Hole Login

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.

Pi-Hole Dashboard in Kubernetes

Als nächstes muss der DNS Server getestet werden. Das kann beispielsweise durch das Werkzeug dig erfolgen eine Alternative ist z.B. nslookup.

Test Abfrage von Pi-Hole DNS in Kubernetes
$ 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.

Pi-Hole Querylog

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.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.