With traffic charts gone from the console, deploy/grafana/fancy-dns.json is where they live: queries by outcome, transport and client group, latency quantiles, RCODEs, cache hits/misses/stale, the upstream round trips saved by coalescing and early refresh, upstream health and latency, blocklist and zone sizes, and (in a Loki row) the blocked/failed-query access log. A node variable spreads or narrows every panel, and the per-node panel shows whether load is even across pods. A test reads the dashboard and fails if a panel queries a metric the resolver does not export, so a rename cannot silently empty a panel. The ingress no longer needs the SSE buffering/timeout annotations, and the statefulset comments no longer describe a query log on the volume. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
191 lines
6.8 KiB
YAML
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 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"
|
|
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) 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
|