build / build-push (push) Successful in 30s
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.
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 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
|