Files
fancy-dns/deploy/k8s/10-deployment.yaml
T
mats 87b6946ab7
build / build-push (push) Successful in 30s
Support RPZ address overrides for local DNS
RPZ A records were parsed as address redirects, but the query pipeline treated the address as a CNAME target and returned the wrong result. Answer IP-literal redirects directly with the matching A or AAAA record, and return NODATA for other query types.

Add the requested EdgeRouter domain overrides as a local RPZ source so each bare owner covers its apex and all subdomains. Mount that source into the pods and document the address-record form. When the Zones page has no existing zone, start the Add Record form in New zone mode and tell the operator to enter its apex.
2026-09-28 15:37:46 +02:00

191 lines
6.8 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"
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--<apex>.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