Files
fancy-dns/deploy/k8s/10-deployment.yaml
T
matsandClaude Sonnet 5.5 04ebf68c0f Deploy: read cluster objects, drop the zones and RPZ ConfigMaps
DO NOT SYNC THIS BEFORE MIGRATING. Removing fancy-dns-zones and fancy-dns-rpz
from the kustomization lets a pruning GitOps tool delete them, and they hold the
only copy of the zones and address redirects until the objects exist. Follow
deploy/MIGRATION.md: install the definitions, run `fancy-dns-console migrate`
against the live cluster, then roll this out.

The StatefulSet starts the resolver with -kube and no longer mounts the two
ConfigMaps; its service account loses the ConfigMap role and keeps only the
read-only Role on the fancy-dns objects (crds/rbac.yaml). The kustomization now
installs the definitions and that Role. The shipped config no longer lists the
migrated sources.

Config validation no longer refuses an enabled blocklist with no sources in the
file, because BlockList, DomainRule and AddressOverride objects can supply every
rule and the file cannot know. The existing "no source" warning says so. That
removed the only validation message the settings tests used as a fixture; they
now use an invalid acl.default_policy, and an empty settings document is refused
by the handler instead of by that check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 10:19:47 +02:00

177 lines
6.4 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.
replicas: 1
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