Deploy Pi-Hole on Kubernetes

Pi-Hole is a network-wide ad blocker. It works by blocking ad’s at the DNS level instead of the browser level. Once we’re set up with Pi-Hole’s deployment, you’d change your DNS server either at your local workstation or in your router.

We’ll start off by creating our many manifest file’s that we’ll then apply. There is a helm chart for Pi-Hole located in https://github.com/MoJo2600/pihole-kubernetes/tree/master/charts/pihole, but to be honest, When I initially set up my deployment of Pi-Hole, I wasn’t a fan of the helm chart. I also initially set it up in a very insecure manner.

Before we do anything, let’s create another folder in ~/k3s/ titled “pihole” to keep our manifests tidy and clean. Once that’s completed, We’ll start with by creating the pihole namespace.

Namespace:

Personally, I like to create a namespace for each of my applications. It makes things tidy and clean, along with simplifying the debugging process(less clutter).

let’s cd into our ~/k3s/pihole/ folder and create a namespace.pihole.yaml file. Once that’s been completed, let’s input the following code block in to the namespace.pihole.yaml file:

apiVersion: v1
kind: Namespace
metadata:
  name: pihole
  labels:
    name: pihole

Once the file has been created with the above code, let’s go ahead and apply it with $ kubectl apply -f namespace.pihole.yaml

Now let’s confirm the namespace was created successfully:

$ kubectl get ns

NAME                   STATUS   AGE
default                Active   39h
kube-system            Active   39h
kube-public            Active   39h
kube-node-lease        Active   39h
metallb-system         Active   36h
cert-manager           Active   10h
ingress-nginx          Active   9h
kubernetes-dashboard   Active   8h
pihole                 Active   1m

If done successfully, you should see pihole in the output.

Storage:

My local deployment is set up with a NFS drive which my deployments save data to. To see how you can setup a NFS drive for persistent storage, check out here: https://mujisayed.com/kubernetes/learn_by_doing/initial_setup/setup_nfs/ & https://mujisayed.com/kubernetes/learn_by_doing/initial_setup/setup_nfs_provisioner/

Persistent Volume Claim

We’ll create the persistent volume claim yaml file next. I’ve titled mine pvc.pihole.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  namespace: "pihole"
  name: "pihole-pvc"
spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: "1Gi"                            

Once we’ve created the persistent volume claim, we can go ahead and apply it with $ kubectl apply -f pvc.pihole.yaml

Confirm the PVC was created successfully:

$ kubectl get pvc -n pihole

NAME         STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pihole-pvc   Bound    pihole-pv   1Gi        RWO            nfs-client     23m

Services

Now we’re ready to create our services. Let’s expose some ports for both our DNS traffic, along with access to the web UI. We’ll first create a service for port 53 that will route UDP traffic to our PI-Hole application. Note, we’ll only be exposing the UDP DNS Port. The second part of the code block will expose port 80 for webui traffic.

Let’s create one file to create all of our services. I’ve titled mine svc.pihole.yaml and have inputted the code below:

## Pi-Hole DNS UDP 
apiVersion: v1
kind: Service
metadata:
  name: pihole-dns-udp
  namespace: pihole
spec:
  selector:
    app: pihole
  ports:
    - protocol: UDP
      port: 53
      targetPort: 53
  type: LoadBalancer
---
## Pi-Hole Web
apiVersion: v1
kind: Service
metadata:
  name: pihole-web
  namespace: pihole
spec:
  selector:
    app: pihole
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer

Once our yaml file has been created and populated, we can apply it with $ kubectl apply -f svc.pihole.yaml

Confirm Services applied correctly:

$ kubectl get svc -n pihole
NAME             TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)        AGE
pihole-dns-udp   LoadBalancer   10.43.242.159   192.168.122.201   53:30789/UDP   22m
pihole-web       LoadBalancer   10.43.79.196    192.168.122.202   80:31685/TCP   22m

ConfigMap

With our PV’s and PVC’s set, we can now set our environment variables inside a configmap. I’ve titled mine configmap.pihole.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: pihole-config
  namespace: pihole
  labels:
    app: pihole
data:
    TZ: "America/New_York"                #Set your timezone by replacing America/New_York. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for more timezones
    VIRTUAL_HOST: "pi.hole"               #Location of the admin portal
    PROXY_LOCATION: "pi.hole"
    ServerIP: "192.168.122.202"           #Set ServerIP to the External IP address for pihole-web that we got from the above confirmation
    DNSSEC: "false"                       #Default is false, change to true to enable DNSSEC support
    PIHOLE_DNS_: "8.8.8.8;8.8.4.4"        #Upstream DNS Server, seperate by semicolon ";"
    WEB_PORT: "80"                        #container port for Web UI

Once we’ve set the ConfigMap, let’s go ahead and deploy it with $ kubectl apply -f configmap.pihole.yaml

Confirm the ConfigMap was applied successfully:

$ kubectl get configmap -n pihole

NAME               DATA   AGE
kube-root-ca.crt   1      65m
pihole-config      7      63m

Secrets

MAKE SURE YOU CHANGE THE PASSWORD LISTED IN THE CODE BELOW TO SOMETHING MORE SECURE! You have been warned!

We’ll need to create a secret for our WebUI Admin Password. Lets create a secrets.pihole.yaml file and input the code below:

apiVersion: v1
kind: Secret
metadata:
  name: webui-password 
  namespace: pihole
type: Opaque
stringData:
  WEBPASSWORD: Password12345!              #NOTE! You will need to change this password, especially considering this is definitely not secure!

Once the secrets.pihole.yaml file has been created, let’s apply it with $ kubectl apply -f secrets.pihole.yaml

Now let’s confirm the secret was applied successfully:

$ kubectl get secrets -n pihole

NAME                  TYPE                                  DATA   AGE
default-token-2l9tx   kubernetes.io/service-account-token   3      65m
webui-password        Opaque                                1      64m

Deployment!

Phew, with all of that out of the way, we’re now ready to deploy our application! Since we want to deploy our application with persistent storage, It’d be best to deploy it as a statefulset. Let’s go ahead and create our sfs.pihole.yaml and populate it!

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pihole
  namespace: pihole
  labels:
    app: pihole

spec:
  selector:
    matchLabels:
      app: pihole
  serviceName: pihole
  replicas: 1
  template:
    metadata:
      labels:
        app: pihole

    spec:
      containers:
      - name: pihole
        image: pihole/pihole:latest
        imagePullPolicy: IfNotPresent
        ports:
        - name: pihole-web
          containerPort: 80
          protocol: TCP
        - name: pihole-dns-udp
          containerPort: 53
          protocol: UDP
        - name: pihole-dns-tcp
          containerPort: 53
          protocol: TCP
        volumeMounts:
          - name: pihole-data
            mountPath: /etc/pihole

        envFrom:
          - configMapRef:
                name: pihole-config
          - secretRef:
                name: webui-password
        livenessProbe:
            httpGet:
                path: /admin.index.php
                port: pihole-web
            initialDelaySeconds: 60
            failureThreshold: 10
            timeoutSeconds: 5

        readinessProbe:
            httpGet:
                path: /admin.index.php
                port: pihole-web
            initialDelaySeconds: 60
            failureThreshold: 3
            timeoutSeconds: 5


      volumes:
        - name: pihole-data
          persistentVolumeClaim:
            claimName: pihole-pvc

Now we’re ready to deploy our application! let’s deploy it with $ kubectl apply -f sfs.pihole.yaml

Now for a final check, let’s check to see it’s all deployed successfully!

$ kubectl get all -n pihole
NAME           READY   STATUS    RESTARTS   AGE
pod/pihole-0   1/1     Running   0          6m27s

NAME                     TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)        AGE
service/pihole-dns-udp   LoadBalancer   10.43.242.159   192.168.122.201   53:30789/UDP   50m
service/pihole-web       LoadBalancer   10.43.79.196    192.168.122.202   80:31685/TCP   50m

NAME                      READY   AGE
statefulset.apps/pihole   1/1     64m

confirm DNS queries are being resolved by Pi-Hole:

$ dig pi-hole.net @192.168.122.201                                              #NOTE! the IP after the @ is the external ip of pihole-dns-udp service

; <<>> DiG 9.16.1-Ubuntu <<>> pi-hole.net @192.168.122.201
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39254
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;pi-hole.net.                   IN      A

;; ANSWER SECTION:
pi-hole.net.            300     IN      A       3.18.136.52

;; Query time: 19 msec
;; SERVER: 192.168.122.201#53(192.168.122.201)
;; WHEN: Fri Mar 11 13:17:34 UTC 2022
;; MSG SIZE  rcvd: 56

Once you’ve changed your DNS Servers either on your local workstation, or at the router, go ahead and visit http://pi.hole/admin(or if you haven’t, go to the IP address of service/pihole-web, e.g http://192.168.122.202/admin) and see your first deployment!