apiVersion: apps/v1 kind: StatefulSet metadata: name: fancy-dns namespace: dns labels: app.kubernetes.io/name: fancy-dns annotations: k8s-helper.io/git-url: "https://git.k6n.net/mats/fancy-dns" k8s-helper.io/git-ssh: "git@git.k6n.net:mats/fancy-dns.git" spec: serviceName: fancy-dns # A StatefulSet rather than a Deployment because each pod owns a PVC (the # cache snapshot and the query log are per-pod state worth keeping across a # reschedule). The flip side: the HPA does not work on a StatefulSet — the # controller and the autoscaler fight over the replica count — so scaling # means adding nodes and replicas here, not CPU targets. replicas: 2 podManagementPolicy: Parallel selector: matchLabels: app.kubernetes.io/name: fancy-dns template: metadata: labels: app.kubernetes.io/name: fancy-dns annotations: k8s-helper.io/git-url: "https://git.k6n.net/mats/fancy-dns" k8s-helper.io/git-ssh: "git@git.k6n.net:mats/fancy-dns.git" dns.k6n.net/zones-api: "enabled" # A config change is a SIGHUP, not a restart: the server re-reads the # file and hot-swaps every component. This annotation is for # stakater/reloader-class controllers; without one, `kubectl rollout # restart` is the manual equivalent. reloader.stakater.com/auto: "true" spec: serviceAccountName: fancy-dns nodeSelector: kubernetes.io/arch: amd64 imagePullSecrets: - name: reg-k6n # The pod listens on high ports; Services expose the standard DNS ports. # No hostNetwork, so replicas can share nodes without port collisions. dnsPolicy: ClusterFirst # The app drains in-flight queries for up to 15s on SIGTERM (see the # shutdown timeout in cmd/fancy-dns/main.go); give it room beyond that. terminationGracePeriodSeconds: 30 securityContext: runAsNonRoot: true runAsUser: 65532 runAsGroup: 65532 fsGroup: 65532 seccompProfile: type: RuntimeDefault topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app.kubernetes.io/name: fancy-dns containers: - name: fancy-dns image: registry.k6n.net/fancydns imagePullPolicy: Always args: - -config - /etc/fancy-dns/config.yaml env: # Substituted into the config by the loader, so the admin API key # comes from the Secret and not from the ConfigMap. - name: FANCYDNS_API_KEY valueFrom: secretKeyRef: name: fancy-dns-apikey key: api-key ports: - name: dns-udp containerPort: 1053 protocol: UDP - name: dns-tcp containerPort: 1053 protocol: TCP - name: dot containerPort: 1853 protocol: TCP - name: doh containerPort: 8443 protocol: TCP - name: admin containerPort: 8080 protocol: TCP securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] # Liveness: the process is up. /api/healthz is unauthenticated on # purpose, because a probe cannot carry an API key. livenessProbe: httpGet: path: /api/healthz port: admin initialDelaySeconds: 5 periodSeconds: 20 # Readiness: the listener is actually bound, so the Service does not # send traffic to a pod that is still binding. readinessProbe: httpGet: path: /api/healthz port: admin initialDelaySeconds: 3 periodSeconds: 5 failureThreshold: 2 # Startup: binding the listeners and fetching the first blocklist # can take a while on a cold node, so give it room before the # liveness probe starts killing it. startupProbe: httpGet: path: /api/healthz port: admin periodSeconds: 3 failureThreshold: 30 resources: requests: cpu: 250m memory: 256Mi limits: # No CPU limit on purpose: a DNS server that gets throttled # turns into dropped queries, which is worse than a noisy # neighbour. The memory limit is the one that matters, because # the cache is sized in entries. memory: 1Gi volumeMounts: - name: config mountPath: /etc/fancy-dns/config.yaml subPath: config.yaml readOnly: true - name: zones mountPath: /etc/fancy-dns/zones readOnly: true - name: rpz mountPath: /etc/fancy-dns/rpz readOnly: true - name: tls mountPath: /etc/fancy-dns/tls readOnly: true - name: data mountPath: /var/lib/fancy-dns - name: tmp mountPath: /tmp volumes: - name: config configMap: name: fancy-dns-config # Local zones are ordinary master files. The API writes them to the # shared ConfigMap, which projects one zone--.zone file per zone here. # The GitOps manifest creates the ConfigMap empty; the UI owns its data. - name: zones configMap: name: fancy-dns-zones optional: false - name: rpz configMap: name: fancy-dns-rpz optional: false - name: tls secret: secretName: fancy-dns-tls - name: tmp emptyDir: {} volumeClaimTemplates: # Cache snapshot (cache.persist.path) and query log (qlog.path) live here, # along with downloaded blocklists. ReadWriteOnce is per-pod: a pod keeps # its own cache across a reschedule, which is the whole point of the # snapshot, but it cannot be shared between replicas. # # Sizing: cache.size is an entry count, not bytes, so disk usage has to be # estimated. A packed A/AAAA answer with a small authority section is # roughly 300-600 bytes on the wire and nearer 1 kB resident, so 500k # entries lands around 500 MB; qlog is the other consumer (see # qlog.max_entries). 4Gi leaves room for both plus a re-fetched list. - metadata: name: data spec: storageClassName: nfs-large accessModes: ["ReadWriteOnce"] resources: requests: storage: 4Gi