Files
fancy-dns/deploy/k8s/10-deployment.yaml
mats 86a83302b6
build / build-push (push) Successful in 42s
Keep two DNS replicas under GitOps
An imperative scale left kubectl owning the replica count while GitOps still declared one, causing apply conflicts. Declare two replicas so DNS remains available across a node failure and GitOps can reconcile the intended count.
2026-11-01 18:01:08 +01:00

178 lines
6.5 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 downloaded blocklists 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.
# Keep DNS available if a node disappears and its pod cannot terminate.
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
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
# Read ClientGroup, BlockList, DNSZone, AddressOverride and
# DomainRule objects from this namespace with the pod's service
# account. The console (console/console.yaml) is what writes them.
- -kube
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: 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
- name: tls
secret:
secretName: fancy-dns-tls
- name: tmp
emptyDir: {}
volumeClaimTemplates:
# Cache snapshot (cache.persist.path) lives here, along with downloaded
# blocklists. The resolver stores no query log: the access log goes to stdout. 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. 4Gi leaves room for that plus a re-fetched
# list.
- metadata:
name: data
spec:
storageClassName: nfs-large
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 4Gi