Skip to content

Rework CoreDNS deployment into a helm-release #14

Closed

Description

@github-actions

# TODO: Rework CoreDNS deployment into a helm-release

# TODO: Rework CoreDNS deployment into a helm-release
apiVersion: apps/v1
kind: Deployment
metadata:
  name: coredns
  namespace: kube-system
  labels:
    k8s-app: kube-dns
  annotations:
    deployment.kubernetes.io/revision: "1"
    configmap.reloader.stakater.com/reload: "coredns"
spec:
  progressDeadlineSeconds: 600
  replicas: 2
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: kube-dns
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        k8s-app: kube-dns
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: k8s-app
                      operator: In
                      values:
                        - kube-dns
                topologyKey: kubernetes.io/hostname
              weight: 100
      containers:
        - args:
            - -conf
            - /etc/coredns/Corefile
          image: registry.k8s.io/coredns/coredns:v1.14.3@sha256:884b72dd6d2f7d367902af420605e0288dffedb0516ce29330423ae3f8f5c6fa
          imagePullPolicy: IfNotPresent
          livenessProbe:
            failureThreshold: 5
            httpGet:
              path: /health
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 60
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 5
          name: coredns
          ports:
            - containerPort: 53
              name: dns
              protocol: UDP
            - containerPort: 53
              name: dns-tcp
              protocol: TCP
            - containerPort: 9153
              name: metrics
              protocol: TCP
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /ready
              port: 8181
              scheme: HTTP
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          resources:
            limits:
              memory: 170Mi
            requests:
              cpu: 100m
              memory: 70Mi
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              add:
                - NET_BIND_SERVICE
              drop:
                - ALL
            readOnlyRootFilesystem: true
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          volumeMounts:
            - mountPath: /etc/coredns
              name: config-volume
              readOnly: true
      dnsPolicy: Default
      nodeSelector:
        kubernetes.io/os: linux
      priorityClassName: system-cluster-critical
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      serviceAccount: coredns
      serviceAccountName: coredns
      terminationGracePeriodSeconds: 30
      tolerations:
        - key: CriticalAddonsOnly
          operator: Exists
        - effect: NoSchedule
          key: node-role.kubernetes.io/control-plane
      volumes:
        - configMap:
            defaultMode: 420
            items:
              - key: Corefile
                path: Corefile
            name: coredns
          name: config-volume

Activity

  1. devantler commented on Jun 8, 2026

    @devantler
    Contributor

    🤖 Generated by the Daily AI Assistant

    Design analysis & decomposition — CoreDNS raw manifests → HelmRelease

    I scoped this against the live tree and the upstream chart so the rework lands as a faithful, reviewable swap rather than a behaviour change to system-critical DNS. Summary: expressible with the official chart, but two ownership decisions need your call before implementation — they're not mechanical.

    Current state (what we actually manage)

    The platform deliberately replaces only the CoreDNS workload + config, leaving the rest distro-managed:

    • k8s/bases/.../coredns/corefile-config-map.yaml — the Corefile ConfigMap
    • k8s/providers/docker/.../coredns/deployment.yaml — the Deployment (image pinned registry.k8s.io/coredns/coredns:v1.14.3@sha256:884b…, 2 replicas, anti-affinity, priorityClassName: system-cluster-critical, CriticalAddonsOnly/control-plane tolerations, RO-rootfs + NET_BIND_SERVICE, reloader annotation configmap.reloader.stakater.com/reload: coredns)
    • k8s/providers/docker/.../coredns/pod-disruption-budget.yaml — minAvailable: 1

    There is no Service, ServiceAccount, ClusterRole/Binding in our tree — the k3d/distro bootstrap owns the kube-dns Service (with its fixed clusterIP that kubelet hands every pod via --cluster-dns), the coredns ServiceAccount, and CoreDNS RBAC.

    The crux — the upstream chart's default ownership ≠ ours

    The official coredns/coredns chart defaults to isClusterService: true (renders the Service as kube-dns in kube-system, k8s-app: kube-dns), which is the right mode. But its defaults differ from our scope:

    • serviceAccount.create: false ✅ matches us (uses existing coredns SA).
    • rbac.create: true ✋ the chart would create ClusterRole/Binding we don't currently own.
    • The chart always renders a Service — there is no "don't manage the Service" flag. On a cluster where k3d already created kube-dns, Flux/Helm must adopt that pre-existing Service (it needs the meta.helm.sh/release-name+-namespace annotations and app.kubernetes.io/managed-by: Helm label, or Helm refuses to take it over), and we must pin service.clusterIP to the distro's kube-dns IP (k3d default 10.43.0.10) so the address kubelet points pods at never changes.

    These are runtime/live-migration facts that a helm template diff cannot prove safe — hence framing them here rather than guessing them in a PR.

    Proposed values mapping (concrete)

    Following the house HelmRelease+HelmRepository pattern (cf. keda/), coredns/ becomes:

    • helm-repository.yaml → https://coredns.github.io/helm
    • helm-release.yaml values:
      • image.repository/tag + image.sha → keep the exact v1.14.3@sha256:884b… pin
      • replicaCount: 2, podDisruptionBudget.minAvailable: 1
      • resources (limits memory: 170Mi; requests cpu: 100m/memory: 70Mi), the preferredDuringScheduling anti-affinity, the two tolerations, priorityClassName: system-cluster-critical
      • podAnnotations.configmap.reloader.stakater.com/reload: coredns (note: with the chart owning the ConfigMap, the reloader target name may change — verify, or drop reloader since Helm rolls the Deployment on Corefile change anyway)
      • servers: → map the current Corefile block-for-block (health{lameduck 5s}, ready, kubernetes … ttl 30, prometheus :9153, forward . /etc/resolv.conf {max_concurrent 1000}, cache 30 {disable success/denial cluster.local}, loop, reload, loadbalance). This is the fiddliest part — needs a careful diff so semantics are byte-equivalent.
      • service.clusterIP: ${coredns_cluster_ip:=10.43.0.10} (Flux substitution), serviceAccount.create: false

    Two decisions for you (the ownership calls)

    1. Service adoption strategy on an already-bootstrapped k3d cluster. Options: (a) annotate/label the distro kube-dns Service for Helm adoption + pin service.clusterIP; (b) configure the distro to skip its CoreDNS so the chart owns everything from a clean slate; (c) keep the Service distro-managed and only Helm-manage Deployment+ConfigMap (not cleanly supported by the chart — likely rules the chart out). I lean (a) for templates targeting fresh bootstraps.
    2. RBAC ownership — rbac.create: true (chart takes over ClusterRole/Binding) vs false (keep distro-managed). (a) is cleaner long-term but means adopting/colliding with the distro's existing RBAC.

    Validation plan

    • Offline: helm template coredns coredns/coredns -f values.yaml and diff the rendered Deployment/ConfigMap/Service/PDB against the current manifests — confirm image pin, Corefile semantics, resources, probes, anti-affinity, tolerations, clusterIP all match.
    • Live (maintainer, at promotion): bootstrap a k3d cluster, confirm DNS resolution stays up through the cutover (no pod loses kube-dns), and kubectl -n kube-system get svc kube-dns keeps the original clusterIP.

    Acceptance criteria

    • coredns/ mirrors the keda/ HelmRelease+HelmRepository layout; the raw deployment.yaml/pod-disruption-budget.yaml/corefile-config-map.yaml are removed.
    • Rendered output is behaviour-equivalent to today (image, Corefile, clusterIP, replicas, PDB, scheduling).
    • The stale # TODO: Rework CoreDNS deployment into a helm-release is gone.

    Once you pick the Service-adoption + RBAC ownership path, the implementation is mechanical and I can ship it as a helm template-diff-validated draft. Flagging the chart-cruft cleanup as a separate small win regardless: the raw deployment.yaml carries kubectl get -o yaml export noise (deployment.kubernetes.io/revision, creationTimestamp: null, schedulerName: default-scheduler, default terminationMessage*) that should drop out in the conversion.

  2. devantler commented on Jun 14, 2026

    @devantler
    Contributor

    🤖 Generated by the Daily AI Assistant

    Design pass: CoreDNS → Flux HelmRelease

    Scoped this next item with a read-only investigation. Summary, chart mapping, and the gotchas that make this a design-first / incremental change rather than a one-shot rewrite.

    Why this is worth doing (the real motivation = consistency)

    CoreDNS is the only controller under k8s/.../infrastructure/controllers/ still shipped as a hand-rolled Deployment + Corefile ConfigMap. Every one of the other ~20 controllers (cert-manager, cilium, metrics-server, kube-prometheus-stack, …) is a Flux HelmRelease with the repo's standard remediation/PDB/topology conventions. So this is a drift/consistency fix: bring the lone outlier onto the established pattern, and inherit the chart's upgrade path instead of pinning a raw image + Corefile by hand.

    ⚠️ Cross-repo scope — platform-first, not template-only

    The live devantler-tech/platform repo carries the identical raw-Deployment CoreDNS (k8s/providers/docker/infrastructure/controllers/coredns/deployment.yaml, base corefile-config-map.yaml, plus a Hetzner Corefile patch). If we convert only the template, the template diverges from platform. The correct order is: prototype + validate on platform (it has a real cluster + CD), then propagate the proven HelmRelease here. This template change should land after the platform one so the scaffold mirrors a battle-tested config.

    Proposed chart mapping (coredns/coredns)

    Add k8s/bases/infrastructure/controllers/coredns/helm-release.yaml (+ a HelmRepository/OCIRepository source, per the repo convention), mirroring metrics-server/helm-release.yaml. Values must reproduce today's behavior exactly:

    • replicaCount: 2; podDisruptionBudget: { minAvailable: 1 } (replaces the standalone PDB).
    • k8sAppLabel: kube-dns and service.name: kube-dns so the Service keeps its identity.
    • service.clusterIP must be pinned to the cluster DNS IP (10.96.0.10) — the chart otherwise allocates a fresh ClusterIP, which breaks every pod's /etc/resolv.conf. This is the single biggest migration hazard.
    • priorityClassName: system-cluster-critical, the CriticalAddonsOnly + control-plane tolerations, the k8s-app=kube-dns podAntiAffinity, resources (req 100m/70Mi, lim 170Mi), and the hardened securityContext (drop ALL, add NET_BIND_SERVICE, readOnlyRootFilesystem, allowPrivilegeEscalation:false) — all expressible via chart values.
    • Pin image/chart version to the digest already in use (coredns v1.14.3).
    • Corefile via the chart's servers: block — the chart default is close but NOT identical. Our Corefile has a non-default cache block that must be carried verbatim:
      cache 30 {
        disable success cluster.local
        disable denial cluster.local
      }
      
      plus forward . /etc/resolv.conf { max_concurrent 1000 }, kubernetes … { pods insecure; fallthrough …; ttl 30 }, health { lameduck 5s }, prometheus :9153, ready, loop, reload, loadbalance. The Hetzner provider's Corefile patch must map to a provider-level values override.

    Validation plan (behavioral DNS correctness is NOT CI-catchable here)

    kubeconform/Validate-Scaffold only checks the HelmRelease CR schema, not the rendered chart — so CI will not catch a wrong Corefile or ClusterIP. Required steps:

    1. helm template coredns coredns/coredns --version <v> -f values.yaml and diff the rendered Corefile byte-for-byte against the current ConfigMap.
    2. Confirm the rendered Service keeps clusterIP: 10.96.0.10.
    3. On a live cluster: roll out, then smoke-test in-cluster resolution (kubernetes.default, an external name) and confirm no pod DNS disruption during the cutover (the chart's Deployment replaces the managed one — watch for adoption vs. recreate).

    Acceptance criteria

    • platform CoreDNS runs as a HelmRelease with the Corefile + ClusterIP + criticality preserved; live DNS verified.
    • This template mirrors the proven config (base helm-release.yaml + source; docker/hetzner providers via values overrides); raw deployment.yaml + standalone PDB removed; corefile-config-map.yaml folded into chart values.
    • helm template | kubeconform -strict clean; Validate Scaffold green.

    Recommend tackling platform first; happy to draft that PR once prioritized.

  3. removed their assignment
    on Jun 23, 2026
  4. devantler commented on Jul 7, 2026

    @devantler
    Contributor

    🤖 Generated by the Daily AI Assistant

    Investigated the migration to scope it safely — two non-obvious couplings make the naive "swap the Deployment for a HelmRelease" break cluster DNS, so recording them here to make this actionable.

    Current shape (docker/Talos provider)

    • k8s/bases/…/coredns/corefile-config-map.yaml — ConfigMap coredns (the Corefile), shared with the hetzner provider (hetzner only patches it, keeps Talos's own Deployment).
    • k8s/providers/docker/…/coredns/{deployment.yaml,pod-disruption-budget.yaml} — the Deployment coredns (+ PDB) that overrides Talos's bootstrapped CoreDNS Deployment (same name/namespace kube-system).
    • Talos (not this repo) provides the kube-dns Service, the coredns ServiceAccount, and CoreDNS RBAC.

    Sharp edges (each one is a DNS outage if missed)

    1. The upstream coredns/coredns chart cannot render "Deployment only." It always emits a Service and a ConfigMap (there is no service.enabled / configmap.enabled toggle — confirmed against the chart's values.yaml). So the HelmRelease must own the whole CoreDNS stack for docker (Deployment + Service + ConfigMap + PDB), which means dropping the shared base ConfigMap from the docker kustomization (base stays hetzner-only) and moving the Corefile into the chart's servers:/config values.
    2. The Service ClusterIP must be pinned to the existing kube-dns IP or DNS breaks. Talos uses its default service subnet 10.96.0.0/12 (no override in talos-local/), so kube-dns is deterministically 10.96.0.10 → set service.clusterIP: 10.96.0.10 and service.name: kube-dns. Set k8sAppLabelOverride: kube-dns so the k8s-app: kube-dns selector (used by the PDB and kube-proxy) is preserved.
    3. Talos reconciles its own CoreDNS, so a Helm-owned Deployment will fight Talos's bootstrapped one unless Talos's built-in CoreDNS is disabled. The migration must also add cluster.coreDNS.disabledManifests / cluster.coreDNS.disabled: true to the Talos machine config (talos-local/cluster/…) — a coupled machine-config change in a different layer from the k8s manifests.

    Proposed first increment

    A single draft PR that: adds a coredns HelmRepository + HelmRelease (values reproducing the current Deployment: replicas: 2, resources, securityContext, anti-affinity, tolerations, priorityClassName: system-cluster-critical, the Corefile, serviceAccount.create: false+name coredns, rbac.create: false, service.clusterIP: 10.96.0.10, k8sAppLabelOverride: kube-dns), disables Talos's built-in CoreDNS, removes the base ConfigMap from the docker kustomization, and keeps the PDB. Static render (ksail workload validate) proves the manifests; the ClusterIP + Talos-ownership swap need a live talos-local bring-up to confirm DNS — so this lands as a draft and its promotion gate is a live DNS check.

    Leaving the hetzner provider untouched (it correctly keeps Talos-managed CoreDNS + a Corefile patch).

  5. github-actions commented on Jul 9, 2026

    @github-actions
    Author

    CoreDNS Deployment -> HelmRelease. Docker-provider only — Talos's own

    CoreDNS is disabled via talos-local/cluster/disable-coredns.yaml (cluster.coreDNS.disabled:

    true) so this chart owns the whole stack (ServiceAccount + RBAC + Deployment + Service +

    ConfigMap + PDB). `coreDNS.disabled` skips Talos's CoreDNS bootstrap entirely on a fresh

    cluster (not just the Deployment) — verified against CodeRabbit's upstream research on

    #62 — so serviceAccount.create/rbac.create must stay true; the chart owns its own identity

    rather than assuming a preexisting Talos-created one. The Service is pinned to the kube-dns

    name + Talos's default kube-dns ClusterIP (10.96.0.10, from the 10.96.0.0/12 service

    subnet) so it adopts the address kubelet/kube-proxy already expect, instead of Talos's

    bootstrapped kube-dns Service. The hetzner provider is untouched (stays on Talos-managed

    CoreDNS + a Corefile patch).

    # TODO(#14): CoreDNS Deployment -> HelmRelease. Docker-provider only — Talos's own
    # CoreDNS is disabled via talos-local/cluster/disable-coredns.yaml (cluster.coreDNS.disabled:
    # true) so this chart owns the whole stack (ServiceAccount + RBAC + Deployment + Service +
    # ConfigMap + PDB). `coreDNS.disabled` skips Talos's CoreDNS bootstrap entirely on a fresh
    # cluster (not just the Deployment) — verified against CodeRabbit's upstream research on
    # #62 — so serviceAccount.create/rbac.create must stay true; the chart owns its own identity
    # rather than assuming a preexisting Talos-created one. The Service is pinned to the kube-dns
    # name + Talos's default kube-dns ClusterIP (10.96.0.10, from the 10.96.0.0/12 service
    # subnet) so it adopts the address kubelet/kube-proxy already expect, instead of Talos's
    # bootstrapped kube-dns Service. The hetzner provider is untouched (stays on Talos-managed
    # CoreDNS + a Corefile patch).

    # TODO(#14): CoreDNS Deployment -> HelmRelease. Docker-provider only — Talos's own
    # CoreDNS is disabled via talos-local/cluster/disable-coredns.yaml (cluster.coreDNS.disabled:
    # true) so this chart owns the whole stack (ServiceAccount + RBAC + Deployment + Service +
    # ConfigMap + PDB). `coreDNS.disabled` skips Talos's CoreDNS bootstrap entirely on a fresh
    # cluster (not just the Deployment) — verified against CodeRabbit's upstream research on
    # #62 — so serviceAccount.create/rbac.create must stay true; the chart owns its own identity
    # rather than assuming a preexisting Talos-created one. The Service is pinned to the kube-dns
    # name + Talos's default kube-dns ClusterIP (10.96.0.10, from the 10.96.0.0/12 service
    # subnet) so it adopts the address kubelet/kube-proxy already expect, instead of Talos's
    # bootstrapped kube-dns Service. The hetzner provider is untouched (stays on Talos-managed
    # CoreDNS + a Corefile patch).
    apiVersion: helm.toolkit.fluxcd.io/v2
    kind: HelmRelease
    metadata:
      name: coredns
      namespace: kube-system
      labels:
        helm.toolkit.fluxcd.io/helm-test: enabled
        helm.toolkit.fluxcd.io/remediation: enabled
    spec:
      interval: 2m
      timeout: 10m
      install:
        remediation:
          retries: -1
      upgrade:
        remediation:
          retries: -1
          remediateLastFailure: true
      chart:
        spec:
          chart: coredns
          version: 1.46.0
          sourceRef:
            kind: HelmRepository
            name: coredns
      # https://github.com/coredns/helm/blob/master/charts/coredns/values.yaml
      values:
        replicaCount: 2
        k8sAppLabelOverride: kube-dns
        image:
          repository: registry.k8s.io/coredns/coredns
          tag: v1.14.3
        serviceAccount:
          create: true
          name: coredns
        rbac:
          create: true
        service:
          name: kube-dns
          clusterIP: 10.96.0.10
        podDisruptionBudget:
          minAvailable: 1
        resources:
          limits:
            memory: 170Mi
          requests:
            cpu: 100m
            memory: 70Mi
        priorityClassName: system-cluster-critical
        nodeSelector:
          kubernetes.io/os: linux
        tolerations:
          - key: CriticalAddonsOnly
            operator: Exists
          - effect: NoSchedule
            key: node-role.kubernetes.io/control-plane
        affinity:
          podAntiAffinity:
            preferredDuringSchedulingIgnoredDuringExecution:
              - podAffinityTerm:
                  labelSelector:
                    matchExpressions:
                      - key: k8s-app
                        operator: In
                        values:
                          - kube-dns
                  topologyKey: kubernetes.io/hostname
                weight: 100
        servers:
          - zones:
              - zone: .
            port: 53
            plugins:
              - name: errors
              - name: health
                configBlock: |-
                  lameduck 5s
              - name: ready
              - name: kubernetes
                parameters: cluster.local in-addr.arpa ip6.arpa
                configBlock: |-
                  pods insecure
                  fallthrough in-addr.arpa ip6.arpa
                  ttl 30
              - name: prometheus
                parameters: :9153
              - name: forward
                parameters: . /etc/resolv.conf
                configBlock: |-
                  max_concurrent 1000
              - name: cache
                parameters: 30
                configBlock: |-
                  disable success cluster.local
                  disable denial cluster.local
              - name: loop
              - name: reload
              - name: loadbalance
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions