My brother wrote up a nice article about how to run a debian mirror on kubernetes. I wanted to take it a step further and make it easier to configure without having to rebuild the image to add another repo. It needs to store gpg keys and some apt sources.list files to make it easy to use the repo. We will also enable directory browsing with NGINX.

Dockerfile

A simple Dockerfile to create the image our cron jobs will use.

FROM debian:10.8-slim

RUN apt-get update
RUN apt-get install -y debmirror gpg

COPY entry.sh /

ENTRYPOINT ["/entry.sh"]
CMD [ "debmirror" ]

Entry.sh script

This is the entry script for the Dockerfile above. It checks to see if we are running debmirror and if we are, add the configured keys and then run debmirror.

#!/bin/sh
set -e
EXECUTABLE=debmirror

if [ "${1#-}" != "$1" ]; then
        set -- ${EXECUTABLE} "$@"
fi

if [ "$1" = "${EXECUTABLE}" ]; then
        shift
        echo "Adding keys: ${KEYS}"
        for key in ${KEYS}
        do
                echo "Adding key: ${key}"
                gpg -q --keyserver keys.gnupg.net --no-default-keyring --keyring trustedkeys.gpg --recv-keys ${key}
        done
        echo "Done adding keys"
        set -- /usr/bin/debmirror ${DEST} --nosource --host=${HOST} --root=${ROOT} --dist=${DIST} --section=${SECTION} --i18n --arch=${ARCH} --passive --cleanup --method=${METHOD} --progress ${OTHERARGS} --rsync-extra=${RSYNCEXTRA:=trace} "$@"
fi

exec "$@"

Kubernetes deployment.yaml

This is the deployment file that will be for the NGINX web server. It will mount the repo, and then the gpg keys and finally the sources.lists.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: debmirror
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: site
      app.kubernetes.io/part-of: debmirror.example.org
  replicas: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: nginx
        app.kubernetes.io/instance: nginx-debmirror.example.org
        app.kubernetes.io/component: site
        app.kubernetes.io/part-of: debmirror.example.org
    spec:
      containers:
      - name: debmirror
        image: nginx:1.19.7-alpine
        livenessProbe:
          httpGet:
            path: /?liveness
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /?readiness
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
          failureThreshold: 20
        resources:
          limits:
            memory: 200Mi
            cpu: 500m
          requests:
            memory: 10Mi
            cpu: 50m
        volumeMounts:
          - mountPath: /usr/share/nginx/html
            name: debmirror-data
          - mountPath: /etc/nginx/conf.d
            name: nginx
          - mountPath: /usr/share/nginx/html/keys
            name: gpg-keys
          - mountPath: /usr/share/nginx/html/sources.list
            name: sources-list
      volumes:
        - name: debmirror-data
          persistentVolumeClaim:
            claimName: debmirror-data
        - name: nginx
          configMap:
            name: nginx
        - name: gpg-keys
          secret:
            secretName: gpg-keys
        - name: sources-list
          configMap:
            name: sources-list

Kubernetes ingress.yaml

This allows external clients access to the NGINX pod.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: debmirror
spec:
  rules:
  - host: deb.example.org
    http:
      paths:
      - backend:
          service:
            name: debmirror
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific

Kubernetes pvc.yaml

This will be where all your repo data is stored. Make sure to set the storage size according to what you need. Here are some examples of what my cloned repos are currently using.

$ du * -sh
24G     apt.kubernetes.io
7.0G    archive.raspberrypi.org
121G    deb.debian.org
5.0G    download.docker.com
74G     raspbian.raspberrypi.org
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: debmirror-data
  labels:
    app.kubernetes.io/name: nginx
    app.kubernetes.io/instance: nginx-debmirror.example.org
    app.kubernetes.io/component: site
    app.kubernetes.io/part-of: debmirror.example.org
spec:
  accessModes:
    - ReadWriteMany
  volumeMode: Filesystem
  resources:
    requests:
      storage: 300Gi
  storageClassName: mystorageclasss

Kubernetes service.yaml

This is to enable port 80/http.

apiVersion: v1
kind: Service
metadata:
  name: debmirror
spec:
  selector:
    app.kubernetes.io/name: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      name: http
  type: ClusterIP

Kubernetes configmap-nginx.yaml

We will use NGINX as a lightweight web server with directory browsing. This is to configure that.

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx
data:
  config.conf: |
    server {
        listen       80;
        listen  [::]:80;

        location / {
            root   /usr/share/nginx/html;
            autoindex on;
        }
    }

Kubernetes cronjob-deb.debian.org.yaml

This is an example cronjob that will run every night at midnight and sync the repo. You can create more cronjobs if you have more repos to sync.

To get the fingerprints of the keys we can use gpg, grep and cut to get them for each file.

$ gpg --show-keys repokeyfile.gpg | grep -Ev 'pub|uid' | cut -c 31-46
6A030B21BA07F4FB

An alternative way is to manually create a job from the cronjob and look through the output for the key. You will need to apply the cronjob to your cluster before doing this.

# Create job from an existing cronjob
$ kubectl create job --from=cronjob/debmirror-download.docker.com testjob -n debmirror
job.batch/testjob created

# get name of testjob pod
$ kubectl get pods -n debmirror
NAME                                                  READY   STATUS      RESTARTS   AGE
debmirror-7f8b4574b8-d4lfn                            1/1     Running     0          115m
debmirror-apt.kubernetes.io-1613952000-vd2jv          0/1     Completed   0          17h
debmirror-deb.debian.org-1613952000-zhxp6             0/1     Completed   0          17h
testjob-bmxm5                                         1/1     Running     0          23s
$ kubectl logs -n debmirror testjob-bmxm5 -f
...
[GNUPG:] NEWSIG
[GNUPG:] ERRSIG 7EA0A9C3F273FCD8 1 10 00 1612219819 9 -
[GNUPG:] NO_PUBKEY 7EA0A9C3F273FCD8
gpgv: Signature made Mon Feb  1 22:50:19 2021 UTC
gpgv:                using RSA key 7EA0A9C3F273FCD8
gpgv: Can't check signature: No public key
...

# Cleanup the temporary pod
$ kubectl delete jobs.batch -n debmirror testjob
job.batch "testjob" deleted

The RSA key 7EA0A9C3F273FCD8 is what you need out of the above example to put into the keys variable in the cronjob.

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: debmirror-deb.debian.org
spec:
  schedule: "0 0 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app.kubernetes.io/name: debmirror
            app.kubernetes.io/instance: debmirror-debmirror.example.org
            app.kubernetes.io/component: sync-debian
            app.kubernetes.io/part-of: debmirror.example.org
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: wlan
          volumes:
            - name: debmirror-data
              persistentVolumeClaim:
                claimName: debmirror-data
          containers:
          - name: debmirror
            image: your.repo.org/debmirror:latest
            imagePullPolicy: Always
            resources:
              limits:
                memory: 1000Mi
                cpu: 1000m
              requests:
                memory: 1000Mi
                cpu: 100m
            volumeMounts:
              - mountPath: /srv
                name: debmirror-data
            env:
              - name: KEYS
                value: "DC30D7C23CBBABEE 4DFAB270CAA96DFA DCC9EFBF77E11517"
              - name: DEST
                value: /srv/deb.debian.org/debian
              - name: HOST
                value: debian.oregonstate.edu
              - name: ROOT
                value: debian
              - name: DIST
                value: buster,buster-updates
              - name: SECTION
                value: main,contrib,non-free,main/debian-installer
              - name: ARCH
                value: amd64,arm64,armhf
              - name: METHOD
                value: http

And here is a cronjob for raspberry pi.

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: debmirror-raspbian.raspberrypi.org
spec:
  schedule: "0 0 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app.kubernetes.io/name: debmirror
            app.kubernetes.io/instance: debmirror-debmirror.example.org
            app.kubernetes.io/component: sync-debian
            app.kubernetes.io/part-of: debmirror.example.org
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: wlan
          volumes:
            - name: debmirror-data
              persistentVolumeClaim:
                claimName: debmirror-data
          containers:
          - name: debmirror
            image: your.repo.org/debmirror:latest
            imagePullPolicy: Always
            resources:
              limits:
                memory: 1000Mi
                cpu: 1000m
              requests:
                memory: 1000Mi
                cpu: 100m
            volumeMounts:
              - mountPath: /srv
                name: debmirror-data
            env:
              - name: KEYS
                value: 9165938D90FDDD2E
              - name: DEST
                value: /srv/raspbian.raspberrypi.org
              - name: HOST
                value: raspbian.raspberrypi.org
              - name: ROOT
                value: raspbian
              - name: DIST
                value: buster
              - name: SECTION
                value: main,contrib,non-free,rpi,firmware
              - name: ARCH
                value: armhf
              - name: METHOD
                value: http

Kubernetes secret-gpg-keys.yaml

This file will allow us to store the GPG keys for repos on our server. Some GPG keys are binary and some are not so we need to just convert them to base 64 format, again if they already are. This makes it simple. We can use kubernetes secrets for the automatic conversion back without having to write any container entry scripts. Use “base64 –wrap=0 repokeyfile.gpg” command to get the long string for each repo gpg key you want to add. You can see how to add them to the secrets file in the example below.

apiVersion: v1
kind: Secret
metadata:
G  name: gpg-keys
data:
  download.docker.com.gpg: |
    LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tCgptUUlOQkZpdDJpb0JFQURoV3BaOC93dlo2aFVUaVhPd1FIWE1BbGFGSGNQSDloQXRyNEYxeTIrT1lkYnRNdXRoCmxxcXdwMDI4QXF5WStQUmZWTXRTWU1ianVRdXU1Ynl5S1IwMUJicVlodVMzanRxUW1salovYkp2WHFubWlWWGgKMzhVdUxhK3owNzdQeHl4UWh1NUJicW50VFBRTWZpeXFFaVUrQkticTJXbUFOVUtRZisxQW1aWS9JcnVPWGJucQpMNEMxK2dKOHZmbVhRdDk5bnBDYXhFamFOUlZZZk9TOFFjaXhOekhVWW5iNmVtamxBTnlFVmxaemVxbzdYS2w3ClVyd1Y1aW5hd1RTeldOdnRqRWpqNG5KTDhOc0x3c2NwTFBRVWhUUSs3QmJRWEF3QW1lSENVVFFJdnZXWHF3ME4KY21oaDRIZ2VRc2NRSFlnT0pqakRWZm9ZNU11Y3ZnbGJJZ0NxZnpBSFc5anhtUkw0cWJNWmorYjFYb2VQRXRodAprdTRiSVFOMVg1UDA3Zk5XemxnYVJMNVo0UE9YRERaVGxJUS9FbDU4ajlrcDRibldSQ0pXMGx5YStmOG9jb2RvCnZaWitEb2krZnk0RDVaR3JMNFhFY0lRUC9MdjV1RnlmK2tRdGwvOTRWRllWSk9sZUF2OFc5MktkZ0RraFRjVEQKRzdjMHRJa1ZFS05VcTQ4YjNhUTY0Tk9aUVc3ZlZqZm9Ld0VaZE9xUEU3MlBhNDVqclp6dlVGeFNwZGlOazJ0WgpYWXVrSGpseHhFZ0JkQy9KM2NNTU5SRTFGNE5DQTNBcGZWMVk3L2hUZU9ubUR1RFl3cjkvb2JBOHQwMTZZbGpqCnE1cmRreXdQZjRKRjhtWFVXNWVDTjF2QUZIeGVnOVpXZW1oQnRRbUd4WG53OU0rejZoV3djNmFobXdBUkFRQUIKdEN0RWIyTnJaWElnVW1Wc1pXRnpaU0FvUTBVZ1pHVmlLU0E4Wkc5amEyVnlRR1J2WTJ0bGNpNWpiMjAraVFJMwpCQk1CQ2dBaEJRSllyZWZBQWhzdkJRc0pDQWNEQlJVS0NRZ0xCUllDQXdFQUFoNEJBaGVBQUFvSkVJMkJnRHdPCnY4Mklzc2tQL2lRWm82OGZsRFFtTnZuOFg1WFRkNlJSYVVIMzNrWFlYcXVUNk5rSEpjaVM3RTJnVEptcXZNcWQKdEk0bU5ZSENTRVl4STVxcmNZVjVZcVg5UDYrS28rdm96bzRuc2VVUUxQSC9BVFE0cUwwWm9rKzFqa2FnM0xnawpqb255VWY5Ynd0V3hGcDA1SEMzR01IUGhoY1VTZXhDeFFMUXZuRldYRDJzV0xLaXZIcDJmVDhRYlJHZVorZDNtCjZmcWNkNUZ1N3B4c3FtMEVVREs1TkwrblBJZ1loTithdVRyaGd6aEsxQ1NoZkdjY00vd2ZSbGVpOVV0ejZwOVAKWFJLSWxXblh0VDRxTkdaTlROMHRSK05MRy82QnFkOE9ZQmFGQVVjdWUvdzFWVzZKUTJWR1laSG5adTlTOExNYwpGWUJhNUlnOVB4d0dRT2dxNlJES0RiVitQcVRRVDVFRk1lUjFtcmpja2s0RFFKamJ4ZU1aYmlOTUc1a0dFQ0E4CmczODNQM2VsaG4wM1dHYkVFYTRNTmMzWjQrN2MyMzZRSTN4V0pmTlBkVWJYUmFBd2h5LzZyVFNGYnp3S0IwSm0KZWJ3elFmd2pRWTZmNTVNaUkvUnFEQ3l1UGozcjNqeVZSa0s4NnBRS0JBSndGSHlxajlLYUtYTVpqZlZub3dMaAo5c3ZJR2ZOYkdIcHVjQVRxUkV2VUh1UWJObnFrQ3g4VlZodFlraERiOWZFUDJ4QnU1VnZIYlIrM25mVmhNdXQ1CkczNEN0NVJTN0p0NkxJZkZkdGNuOENhU2FzL2wxSGJpR2VSZ2M3MFgvOWFZeC9WL0NFSnYwbEllOGdQNnVEb1cKRlBJWjdkNnZIK1ZybzZ4dVdFR2l1TWFpem5hcDJLaFptcGtnZnVweUZtcGxoMHM2a255bXVRSU5CRml0MmlvQgpFQURuZUw5UzltNHZoVTNibGFSalZVVXlKN2IvcVRqY1N5bHZDSDVYVUU2UjJrK2NrRVpqZkFNWlBMcE8rL3RGCk0ySklKTUQ0U2lmS3VTM3hjazlLdFpHQ3VmR21jd2lMUVJ6ZUhGN3ZKVUtyTEQ1UlRrTmkyM3lkdldaZ1BqdHgKUStEVFQxWmNuN0JyUUZZNkZnblJvVVZJeHd0ZHcxYk1ZLzg5cnNGZ1M1d3d1TUVTZDNRMlJZZ2I3RU9GT3BudQp3NmRhN1dha1dmNElobkY1bnNOWUdEVmFJSHpwaXFDbCt1VGJmMWVwQ2pyT2xJemtaM1ozWWs1Q00vVGlGelBrCnoybEx6ODljcEQ4VStOdENzZmFnV1dmamQyVTNqRGFwZ0grN25RbkNFV3BST3R6YUtIRzZsQTNwWGRpeDV6RzgKZVJjNi8wSWJVU1d2ZmpLeExMUGZOZUNTMnBDTDNJZUVJNW5vdGhFRVlkUUg2c3pwTG9nNzl4QjlkVm5KeUtKYgpWZnhYbnNlb1lxVnJSejJWVmJVSTVCbHdtNkI0MEUzZUdWZlVRV2l1eDU0RHNweVZNTWs0MU14N1FKM2l5bklhCjFONFpBcVZNQUVydXlYVFJUeGM5WFcwdFloRE1BLzFHWXZ6MEVtRnBtOEx6VEhBNnNGVnRQbS9abE5DWDZQMVgKekp3cnY3RFNRS0Q2R0dsQlFVWCtPZUVKOHRUa2tmOFFUSlNQVWRoOFA4WXhERlM1RU9HQXZoaHBNQllENDJrUQpwcVhqRUMrWGN5Y1R2R0k3aW1wZ3Y5UERZMVJDQzF6a0JqS1BhMTIwck5odi9oa1ZrL1lodUdvYWpvSHl5NGg3ClpRb3BkY010cE4yZGdtaEVlZ255OUpDU3d4ZlFtUTB6SzBnN202U0hpS013andBUkFRQUJpUVErQkJnQkNBQUoKQlFKWXJkb3FBaHNDQWlrSkVJMkJnRHdPdjgySXdWMGdCQmtCQ0FBR0JRSllyZG9xQUFvSkVINmdxY1B5Yy96WQoxV0FQLzJ3SitSMGdFNnFzY2UzcmphSXo1OFBKbWM4Z29LcmlyNWhuRWxXaFBnYnE3Y1lJc1c1cWlGeUxoa2RwClljTW1oRDltUmlQcFFuNllhMnczZTNCOHpmSVZLaXBiTUJua2UveXRaOU03cUhtRENjam9pU213RVhOM3dLWUkKbUQ5VkhPTnNsL0NHMXJVOUlzdzFqdEI1ZzFZeHVCQTdNL20zNlhONngydStOdE5NREI5UDU2eWM0Z2ZzWlZFUwpLQTl2K3lZMi9sNDVMOGQvV1VrVWkwWVhvbW42aHlCR0k3SnJCTHEwQ1gzN0dFWVA2TzlycktpcGZ6NzNYZk83CkpJR3pPS1psbGpiL0Q5UlgvZzduUmJDbiszRXRIN3huaytUSy81MGV1RUt3OFNNVWcxNDdzSlRjcFFtdjZVeloKY000SmdMMEhiSFZDb2pWNEMvcGxFTHdNZGRBTE9GZVlRelRpZjZzTVJQZiszRFNqOGZyYkluakNoQzN5T0x5MAo2YnI5MktGb20xN0VJajJDQWNvZXE3VVBoaTJvb3VZQndQeGg1eXRkZWhKa29vK3NON1JJV3VhNlAyV1Ntb241ClU4ODhjU3lsWEMwK0FERmRnTFg5SzJ6ckRWWVVHMXZvOENYMHZ6eEZCYUh3TjZQeDI2ZmhJVDEvaFlVSFFSMXoKVmZORGN5UW1YcWtPblp2dm9NZnovUTBzOUJoRkovelU2QWdRYklaRS9obTFzcHNmZ3Z0c0QxZnJaZnlnWEo5ZgppclArTVNBSTgweEhTZjkxcVNSWk9qNFBsM1pKTmJxNHlZeHYwYjFwa01xZUdkamRDWWhMVStMWjR3YlFtcENrClNWZTJwcmxMdXJlaWdYdG1aZmtxZXZSejdGcklaaXU5a3k4d25DQVB3Qzcvem1TMThyZ1AvMTdiT3RMNC9pSXoKUWh4QUFvQU1XVnJHeUppdlNramhTR3gxdUNvanNXZnNUQW0xMVA3anNydUlMNjFaek1VVkUyYU0zUG1qNUcrVwo5QWNaNThFbSsxV3NWbkFYZFVSLy9iTW1oeXI4d0wvRzFZTzFWM0pFSlRSZHhzU3hkWWE0ZGVHQkJZL0FkcHN3CjI0anhoT0pSK2xzSnBxSVVlYjk5OStSOGV1RGhSSEc5ZUZPN0RSdTZ3ZWF0VUo2c3V1cG9EVFJXdHIvNHlHcWUKZEt4VjNxUWhOTFNuYUF6cVcvMW5BM2lVQjRrN2tDYUtaeGhkaERiQ2xmOVAzN3FhUlc0NjdCTENWTy9jb0wzeQpWbTUwZHdkck50S3BNQmgzWnBiQjF1SnZnaTltWHR5Qk9NSjN2OFJaZUR6RmlHOEhkQ3RnOVJ2SXQvQUlGb0hSCkgzUytVNzlOVDZpMEtQekxJbURmczhUN1JscHl1TWM0VWZzOGdneWc5djNBZTZjTjNlUXl4Y0szdzBjYkJ3c2gKL25RTmZzQTZ1dSs5SDdOaGJlaEJNaFlucE5aeXJIekNtenlYa2F1d1JBcW9DYkdDTnlrVFJ3c3VyOWdTNDFUUQpNOHNzRDFqRmhlT0pmM2hPRG5rS1UrSEtqdk1ST2wxREs3emRtTGROekExY3Z0WkgvbkNDOUtQajF6OFFDNDdTCnh4K2RUWlN4NE9OQWh3YlMvTE4zUG9LdG44TFBqWTlOUDl1RFdJK1RXWXF1UzJVK0tIRHJCRGxzZ296RGJzL08KakN4Y3BEek5tWHBXUUhFdEhVNzY0OU9YSFA3VWVOU1QxbUNVQ0g1cWRhbmswVjFpZWpGNi9DZlRGVTRNZmNyRwpZVDkwcUZGOTNNM3YwMUJieFArRUlZMi85dGlJUGJyZAo9MFlZaAotLS0tLUVORCBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tCg==

Kubernetes sources.yaml configmap

This would be in the sources-list config map. This will create a file on the webserver to make it easy for someone to wget it or something and use your repo.

piVersion: v1
kind: ConfigMap
metadata:
  name: sources-list
data:
  debian-buster-amd64.list: |
    deb http://deb.example.org/deb.debian.org/debian buster main contrib non-free
    deb http://deb.example.org/deb.debian.org/debian-security buster/updates main contrib non-free
    deb http://deb.example.org/deb.debian.org/debian buster-updates main contrib non-free

References

  1. https://www.frakkingsweet.com/debmirror-docker-container/
  2. https://www.frakkingsweet.com/debian-mirror-in-kubernetes/