185 lines
6.8 KiB
YAML
185 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"
|
|
# 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
|
|
# No hostNetwork. The server does not set SO_REUSEPORT, so a second
|
|
# host-network pod would fail to bind :53 and crash-loop; the ports are
|
|
# served by a LoadBalancer Service instead (see 20-service.yaml).
|
|
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: fancydns/fancy-dns:latest
|
|
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: 53
|
|
protocol: UDP
|
|
- name: dns-tcp
|
|
containerPort: 53
|
|
protocol: TCP
|
|
- name: dot
|
|
containerPort: 853
|
|
protocol: TCP
|
|
- name: doh
|
|
containerPort: 443
|
|
protocol: TCP
|
|
- name: admin
|
|
containerPort: 8080
|
|
protocol: TCP
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
# Only because of the low ports. The pod is not privileged and
|
|
# has no other capability left; drop the Service and the
|
|
# Capability together if you move the listeners off 53/443.
|
|
add: ["NET_BIND_SERVICE"]
|
|
# 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: 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; ship them in a ConfigMap or
|
|
# manage them through the admin API (`/api/zones`), which writes into
|
|
# the data volume. Optional because the glob in the config matches
|
|
# nothing cleanly.
|
|
- name: zones
|
|
configMap:
|
|
name: fancy-dns-zones
|
|
optional: true
|
|
- 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
|