My continuing venture in to Kubernetes led me to wanting to run DHCP inside of it.

Installing DHCP inside of a Docker container is simple. I chose to use Alpine as my base image because it is small and lightweight.

Here is my simple Dockerfile:

FROM alpine:3.14.2

RUN apk add --no-cache dhcp
VOLUME /etc/bind

CMD ["dhcpd", "-4", "-d", "-cf", "/etc/dhcp/dhcpd.conf"]

The breakdown of the arguments is this

  • -4 means run as an IPv4 DHCP server
  • -d is for Debug, or what we use it for, run in the foreground
  • -cf is for the config file.

Build that image and push it up to your registry.

For K8s, there will be 3 manifests needed to deploy it. The deployment for the pod, a persistent volume claim for the DHCP leases and the secret containing the dhcpd.conf file. The reason we do a secret for the dhcpd.conf file is because you store the ddns update key in there. If you don't need it then you could modify it to be a config map.

The key part in the deployment is to run on the hostNetwork. That way the pod can directly access the network card and respond to DHCP requests. You also need to expose UDP port 67 and have a volume for the leases so it can persist those across pod restarts.

Here is my deployment file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dhcp
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: dhcp
      app.kubernetes.io/part-of: dhcp
  replicas: 1
  revisionHistoryLimit: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: dhcp
        app.kubernetes.io/instance: dhcp
        app.kubernetes.io/component: dhcp
        app.kubernetes.io/part-of: dhcp
    spec:
      hostNetwork: true
      imagePullSecrets:
        - name: exampleregistry
      containers:
      - name: dhcp4
        image: example.azurecr.io/dhcp4:1.0.6
        resources:
          requests:
            memory: 10Mi
            cpu: 10m
          limits:
            memory: 200Mi
            cpu: 500m
        volumeMounts:
          - mountPath: "/var/lib/dhcp"
            name: dhcp-leases
            readOnly: false
          - mountPath: "/etc/dhcp"
            name: dhcp-secrets
            readOnly: true
        ports:
          - containerPort: 67
            hostPort: 67
            protocol: UDP
            name: dhcp
      volumes:
        - name: dhcp-leases
          persistentVolumeClaim:
            claimName: dhcpleases
        - name: dhcp-secrets
          secret:
            secretName: dhcp

The PVC is simple. You can probably get away doing a ReadWriteOnce. But I always do ReadWriteMany on all of my PVC's. It allows for future flexibility if needed.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dhcpleases
  labels:
    app.kubernetes.io/name: dhcpleases
    app.kubernetes.io/instance: dhcpleases
    app.kubernetes.io/component: dhcp
    app.kubernetes.io/part-of: dhcp
spec:
  accessModes:
    - ReadWriteMany
  volumeMode: Filesystem
  resources:
    requests:
      storage: 1Gi
      ```

The secret will contain a single entry, dhcpd.conf. This will contain the config file in its entirety.

apiVersion: v1
kind: Secret
metadata:
  name: dhcp
type: Opaque
stringData:
  dhcpd.conf: |
    option domain-name "example.lan";
    option domain-name-servers 192.168.0.190;

    update-conflict-detection true;

    default-lease-time 600;
    max-lease-time 7200;

    subnet 192.168.0.0 netmask 255.255.255.0 {
    range 192.168.0.20 192.168.0.99;
    option routers 192.168.0.1;

    key "rndc-key" {
      algorithm hmac-md5;
      secret "xxx";
    };

    zone example.lan. {
      primary 192.168.0.190;
      key rndc-key;
    }

    zone 0.168.192.in-addr.arpa. {
      primary 192.168.0.190;
      key rndc-key;
    }

    ddns-update-style standard;
    ddns-dual-stack-mixed-mode true;
    authoritative;

Conclusion

Getting DHCP running in Kubernetes was the final piece of getting my entire network (aside from DNS) running Kubernetes. The reason I did not put my network DNS in Kubernetes is that it is required to start the Kubernetes cluster and pull the image. A chicken and the egg problem arises if I put DNS in it. I do however, have DNS running in a Docker container on a Raspberry Pi. More on that later.

This was a fun project that I kind of enjoyed doing.