Files
fancy-dns/deploy/k8s/10-deployment.yaml
T
mats d53552f855
build / build-push (push) Successful in 21s
deploy: use cluster registry pull secret
2026-09-27 21:56:50 +02:00

188 lines
7.0 KiB
YAML

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