kadet-hub logokadet-hub

external-dns and PowerDNS without Bitnami, with Kapitan

external-dns with PowerDNS, in place of Bitnami's external-dns chart: external-dns writes a record for every annotated Service into the zone example.internal on a PowerDNS Authoritative Server, which keeps its zones in SQLite on a persistent volume. The init containers seed the database from the image and create the zone once. The API key is a Kapitan ref, generated on the first compile into refs/powerdns/api-key (see the valkey recipe for the backend). No operator needed.

Inventory class

inventory/classes/recipes/external-dns-powerdns.yml, as on the main branch of kadet-hub/examples. A target includes it next to the pins below.

# external-dns with PowerDNS, in place of Bitnami's external-dns chart:
# external-dns writes a record for every annotated Service into the zone
# example.internal on a PowerDNS Authoritative Server, which keeps its zones
# in SQLite on a persistent volume. The init containers seed the database
# from the image and create the zone once. The API key is a Kapitan ref,
# generated on the first compile into refs/powerdns/api-key (see the valkey
# recipe for the backend). No operator needed.
classes:
  - recipes.lib.kubernetes

parameters:
  generators:
    kubernetes:
      namespaces:
        dns: {}
  components:
    powerdns:
      namespace: dns
      type: statefulset
      image: docker.io/powerdns/pdns-auth-51:5.1.4
      security_profile: restricted
      workload_security_context:
        runAsUser: 953
        runAsGroup: 953
        fsGroup: 953
        sysctls:
          - {name: net.ipv4.ip_unprivileged_port_start, value: "0"}
      env:
        PDNS_AUTH_API_KEY:
          secretKeyRef:
            key: api-key
      secrets:
        api:
          string_data:
            api-key:
              value: ?{base64:powerdns/api-key||random:str:32}
      init_containers:
        seed:
          image: docker.io/powerdns/pdns-auth-51:5.1.4
          command: [sh, -ec, "[ -f /data/pdns.sqlite3 ] || cp /var/lib/powerdns/pdns.sqlite3 /data/"]
          volume_mounts:
            data:
              mount_path: /data
          resources:
            requests: {cpu: 10m, memory: 16Mi}
            limits: {memory: 64Mi}
        zone:
          image: docker.io/powerdns/pdns-auth-51:5.1.4
          command: [sh, -ec, "pdnsutil zone list example.internal >/dev/null 2>&1 || pdnsutil zone create example.internal ns1.example.internal"]
          volume_mounts:
            data:
              mount_path: /var/lib/powerdns
          resources:
            requests: {cpu: 10m, memory: 32Mi}
            limits: {memory: 128Mi}
      ports:
        dns-udp:
          container_port: 53
          service_port: 53
          protocol: UDP
        dns-tcp:
          container_port: 53
          service_port: 53
        api:
          container_port: 8081
          service_port: 8081
      healthcheck:
        readiness:
          type: tcp
          port: dns-tcp
      resources:
        requests: {cpu: 100m, memory: 128Mi}
        limits: {memory: 256Mi}
      volume_mounts:
        data:
          mount_path: /var/lib/powerdns
      volume_claims:
        data:
          spec:
            accessModes: [ReadWriteOnce]
            resources:
              requests:
                storage: 1Gi
      service:
        type: ClusterIP

    external-dns:
      namespace: dns
      image: registry.k8s.io/external-dns/external-dns:v0.23.0
      security_profile: restricted
      read_only_root_filesystem: true
      workload_security_context:
        runAsUser: 65534
        runAsGroup: 65534
      automount_service_account_token: true
      service_account:
        create: true
      cluster_role:
        rules:
          - apiGroups: [""]
            resources: [services, pods]
            verbs: [get, watch, list]
          - apiGroups: [discovery.k8s.io]
            resources: [endpointslices]
            verbs: [get, watch, list]
          - apiGroups: [networking.k8s.io]
            resources: [ingresses]
            verbs: [get, watch, list]
          - apiGroups: [""]
            resources: [nodes]
            verbs: [list]
      env:
        PDNS_API_KEY:
          secretKeyRef:
            name: powerdns
            key: api-key
      args:
        - --source=service
        - --publish-internal-services
        - --provider=pdns
        - --pdns-server=http://powerdns.dns.svc:8081
        - --pdns-api-key=$(PDNS_API_KEY)
        - --domain-filter=example.internal
        - --registry=txt
        - --txt-owner-id=kadet-hub-recipe
        - --policy=sync
        - --interval=10s
      ports:
        http:
          container_port: 7979
      healthcheck:
        readiness:
          type: http
          port: http
          path: /healthz
      resources:
        requests: {cpu: 50m, memory: 64Mi}
        limits: {memory: 128Mi}

Compile classes

The class includes one per generator; each compiles its generator once, so a target can combine several recipes.

inventory/classes/recipes/lib/kubernetes.yml

# Compiles the kubernetes generator; recipes include this class, so a target that
# combines several recipes compiles it once.
parameters:
  kapitan:
    compile:
      - {input_type: kadet, output_type: yml, input_paths: [lib/kubernetes], output_path: kubernetes/}

Generator pins

The entries of inventory/classes/generators.yml for these generators, pinned by version and digest.

parameters:
  kapitan:
    dependencies:
    - output_path: lib/kubernetes
      source: ghcr.io/kadet-hub/kubernetes:1.2.0@sha256:52017626e77dd5bf7293ce465ff0c3dc7a61dae1ae8353a56a53ffebe1bfb67c
      type: oci

How it is checked

The examples CI verifies the signed index and the attestation of every pinned generator, compiles the target, validates the output against the generators' schemas, deploys it to a new kind cluster with the operators above and fails unless a Service hostname resolves through PowerDNS. The Chainsaw test holds the assertions.

All recipes · Bitnami chart alternatives