Kubernetes - Setup External DNS

Table of Contents

This post is part of our ongoing series of posts for Kubernetes. In this post we are focusing on setting up External DNS, a critical component for automating DNS record management. Although we are working on a local Kubernetes cluster, we have implemented prerequisites in Kubernetes - Routing external traffic to local Kubernetes cluster to enable access to our internal cluster via public ip.

We are using Cloudflare as our DNS Provider and implementing this solution using declarative GitOps principles with Flux.

1. Setup Cloudflare API Key

First, let’s generate an API key from Cloudflare by navigating to Manage Account > Account API Tokens in the Cloudflare dashboard.

We’ll choose Edit DNS template and generate token for a specific zone

2. Setup Kubernetes Secret

Now we’ll configure a Kubernetes secret by creating cluster/default/cloudflare-token.yaml

Note
If you’re unfamiliar with YAML path conventions and definitions, please refer to our guide on GitOps with FluxCD: Kubernetes GitOps with FluxCD - Part 1 - Initial Setup.
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-token
  namespace: kube-system
type: Opaque
stringData:
  CF_API_TOKEN: ***
  CF_API_EMAIL: ***

Let’s encrypt this sensitive data with SOPS and commit the changes to our Git repository.

Note
For detailed instructions on secret management, refer to Kubernetes GitOps with FluxCD - Part 2 - Secret Management using SOPS.

4. Setup External DNS

Now we’ll configure the External DNS deployment by creating cluster/default/external-dns.yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: external-dns
rules:
- apiGroups: [""]
  resources: ["services","endpoints","pods"]
  verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["get","watch","list"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["list","watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: external-dns-viewer
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns
subjects:
- kind: ServiceAccount
  name: external-dns
  namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: k8s.gcr.io/external-dns/external-dns:v0.14.2
        args:
        - --source=ingress
        - --domain-filter=***
        - --provider=cloudflare
        resources:
          requests:
            memory: "64Mi"
            cpu: "20m"
          limits:
            memory: "256Mi"
            cpu: "200m"
        env:
        - name: CF_API_TOKEN
          valueFrom:
            secretKeyRef:
              name: cloudflare-token
              key: CF_API_TOKEN
        - name: CF_API_EMAIL
          valueFrom:
            secretKeyRef:
              name: cloudflare-token
              key: CF_API_EMAIL

Explanation of External DNS Configuration

This YAML manifest consists of four Kubernetes resources that work together to enable automatic DNS record management:

  1. ServiceAccount:

    • Creates a dedicated service account named external-dns in the kube-system namespace
    • This provides an identity for the External DNS process to operate within the cluster
  2. ClusterRole:

    • Defines the permissions required by External DNS to function properly
    • Allows read-only access (get, watch, list) to:
      • Services, endpoints, and pods: To monitor for changes to service endpoints
      • Ingresses: To detect new or modified ingress resources
      • Nodes: To gather information about the cluster nodes
  3. ClusterRoleBinding:

    • Associates the previously defined external-dns ClusterRole with the external-dns ServiceAccount
    • This binding effectively grants the External DNS service the permissions it needs
  4. Deployment:

    • Creates the actual External DNS controller pod with specific configuration:
      • Uses the Recreate strategy to ensure clean upgrades
      • Deploys the official k8s.gcr.io/external-dns/external-dns:v0.14.2 image
      • Configures important command-line arguments:
        • --source=ingress: Instructs External DNS to watch for Ingress resources
        • --domain-filter=***: Limits DNS operations to a specific domain (redacted in the example)
        • --provider=cloudflare: Specifies Cloudflare as the DNS provider
      • Sets resource constraints to optimize performance:
        • Requests modest resources (64Mi memory, 20m CPU)
        • Sets reasonable limits (256Mi memory, 200m CPU)
      • Injects Cloudflare credentials from the previously created secret:
        • CF_API_TOKEN: Authentication token for Cloudflare API access
        • CF_API_EMAIL: Associated email for the Cloudflare account

With this configuration, External DNS will continuously monitor the Kubernetes cluster for new or modified Ingress resources. When it detects changes, it automatically creates, updates, or deletes the corresponding DNS records in Cloudflare according to the annotations and specifications in those resources. This eliminates the need for manual DNS management and ensures that your services are always accessible via their correct domain names.

Let’s commit these changes to our Git repository and verify that External DNS is deployed successfully.

As we can see from the notifications in Discord, External DNS has been configured successfully.

Let’s examine the pod status as well:

kubectl -n kube-system get pods

NAME                                      READY   STATUS      RESTARTS      AGE
coredns-ccb96694c-s67q5                   1/1     Running     4 (60m ago)   4d2h
external-dns-7cbfcc8d94-zwck7             1/1     Running     0             2m3s
helm-install-traefik-8q42l                0/1     Completed   1             4d2h
helm-install-traefik-crd-nq5tj            0/1     Completed   0             4d2h
local-path-provisioner-5cf85fd84d-pxdcv   1/1     Running     4 (60m ago)   4d2h
metrics-server-5985cbc9d7-qn4dr           1/1     Running     4 (60m ago)   4d2h
svclb-traefik-aadbbe88-gjtbx              2/2     Running     8 (60m ago)   4d2h
svclb-traefik-aadbbe88-vzd2t              2/2     Running     6 (60m ago)   4h10m
traefik-5d45fc8cc9-g97pg                  1/1     Running     4 (60m ago)   4d2h

As confirmed, External DNS is up and running. Let’s verify the logs to ensure proper functionality:

kubectl -n kube-system logs pod/external-dns-7cbfcc8d94-zwck7 

time="2025-02-28T11:26:25Z" level=info msg="Instantiating new Kubernetes client"
time="2025-02-28T11:26:25Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2025-02-28T11:26:25Z" level=info msg="Created Kubernetes client https://10.43.0.1:443"
time="2025-02-28T11:26:27Z" level=info msg="All records are already up to date"
time="2025-02-28T11:27:27Z" level=info msg="All records are already up to date"
time="2025-02-28T11:28:28Z" level=info msg="All records are already up to date"

The logs indicate that External DNS is functioning properly.

5. Verify Setup

Now we’ll validate our setup by configuring an ingress for a sample application we created in Kubernetes GitOps with FluxCD - Part 3 - Automated Image Updates.

First, let’s configure a service by creating apps/sample-app/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: sample-app-service
  namespace: default
spec:
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080
  selector:
    app: sample-app

Next, let’s configure the ingress by creating apps/sample-app/ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sample-app-ingress
  namespace: default
  annotations:
    external-dns.alpha.kubernetes.io/target: "******"
spec:
  rules:
    - host: sample-app.***
      http:
        paths:
          - pathType: Prefix
            backend:
              service:
                name: sample-app-service
                port:
                  number: 80
            path: /

Since we’re working with a local cluster, we’ve implemented a solution to route external traffic to our cluster as detailed in Kubernetes - Routing external traffic to local Kubernetes cluster. The public IP of our external server is specified in the external-dns.alpha.kubernetes.io/target.

Now, let’s modify the kustomization file at apps/sample-app/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
   - deployment.yaml
+  - service.yaml
+  - ingress.yaml

Let’s commit these changes and verify that External DNS has created the necessary DNS records:

kubectl -n kube-system logs pod/external-dns-7cbfcc8d94-zwck7 

time="2025-02-28T11:42:34Z" level=info msg="Changing record." action=CREATE record=**** ttl=1 type=A zone=****
time="2025-02-28T11:42:35Z" level=info msg="Changing record." action=CREATE record=**** ttl=1 type=TXT zone=****
time="2025-02-28T11:42:35Z" level=info msg="Changing record." action=CREATE record=**** ttl=1 type=TXT zone=****

The logs confirm that the DNS records have been successfully created.

Finally, let’s verify access to our application using curl:

curl http://sample-app.**

Greetings From K8S App : Version 2

We have successfully configured External DNS, automating the creation and management of DNS records for our Kubernetes services.

What next ?

Future posts will explore advanced Kubernetes and GitOps patterns with FluxCD, including:

  • Setting up Cert Manager
  • Push based reconciliation triggers with Webhook receivers for FluxCD

Stay tuned for each of these topics.

References