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" # 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 imagePullSecrets: - name: reg-k6n # No hostNetwork. The server does not set SO_REUSEPORT, so a second # host-network pod would fail to bind :53 and crash-loop; the ports are # served by a LoadBalancer Service instead (see 20-service.yaml). 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: 53 protocol: UDP - name: dns-tcp containerPort: 53 protocol: TCP - name: dot containerPort: 853 protocol: TCP - name: doh containerPort: 443 protocol: TCP - name: admin containerPort: 8080 protocol: TCP securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] # Only because of the low ports. The pod is not privileged and # has no other capability left; drop the Service and the # Capability together if you move the listeners off 53/443. add: ["NET_BIND_SERVICE"] # 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: 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; ship them in a ConfigMap or # edit them in the mounted volume. There is no zone API — the admin API # drives blocklists, the cache and the query log, not the zone files. # the data volume. Optional because the glob in the config matches # nothing cleanly. - name: zones configMap: name: fancy-dns-zones optional: true - 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