kubernetes kubernetes/networking

External DNS

Core Idea

ExternalDNS watches the cluster and writes your provider’s DNS records for you, so Services and Ingresses get hostnames without anyone hand-editing a zone.

  • Architecture and the RBAC prerequisites it needs to read cluster sources and write DNS entries.
  • Configuration patterns for cluster sources plus multi-cloud provider setups (AWS IAM roles, GCP service accounts).
  • Production hardening includes domain filters so two clusters can manage separate subdomains of the same domain.

Info

ExternalDNS eliminates the manual effort required to create and maintain DNS records when exposing services or ingress endpoints outside the cluster. It ensures that DNS records are dynamically updated as resources in the cluster are created, modified, or deleted.

ExternalDNS is an open-source Kubernetes incubator project (kubernetes-sigs/external-dns) that automates the orchestration of public and private DNS records directly from Kubernetes resources. This guide provides an in-depth operational blueprint for integrating, configuring, and securing ExternalDNS inside production-grade clusters.

1. Architectural Overview

Kubernetes’ built-in DNS (such as CoreDNS) resolves service names internally (*.svc.cluster.local). However, CoreDNS cannot communicate with public registrars or cloud-hosted DNS networks.

ExternalDNS bridges this gap. It operates as a control-loop controller within the cluster, querying the Kubernetes API for resources that expose endpoints, compiling desired records, and synchronizing them with external DNS providers via their respective APIs.

Core Concepts

  1. Sources: The Kubernetes resources ExternalDNS watches. Supported sources include service, ingress, gateway-httproute, gateway-grpcroute, gateway-tlsroute, and Custom Resource Definitions (CRDs) like Linkerd or Istio VirtualServices.
  2. Providers: The cloud or on-premise DNS APIs ExternalDNS interacts with. Supported providers include AWS Route 53, Cloudflare, Google Cloud DNS, Azure DNS, Infoblox, NS1, DigitalOcean, and PowerDNS.
  3. Registry: The mechanism used to track DNS record ownership.
    • TXT Registry (Recommended): Alongside every A or CNAME record, ExternalDNS writes a companion TXT record containing an identifier string (e.g., heritage=external-dns,external-dns/owner=prod-cluster-01). This ensures multiple clusters or external manual operators do not overwrite each other’s records.
    • No-Op Registry: Directly synchronizes resources without tracking state. This is highly risky in multi-tenant or multi-cluster environments.

2. Cluster Prerequisites & RBAC Setup

ExternalDNS requires read access to cluster networking APIs and read/write access to its internal state tracking resources. Below is the standard Role-Based Access Control (RBAC) manifest for a standard cluster deployment.

RBAC Definition

apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: external-dns
rules:
  # Permissions to watch ingress/service states
  - apiGroups: [""]
    resources: ["services","endpoints","pods","nodes"]
    verbs: ["get","watch","list"]
  - apiGroups: ["networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["get","watch","list"]
  # (Optional) Read Namespaces to filter sources
  - apiGroups: [""]
    resources: ["namespaces"]
    verbs: ["get","watch","list"]
  # Required if using [[Gateway API]] sources
  - apiGroups: ["gateway.networking.k8s.io"]
    resources: ["gateways","httproutes","grpcroutes","tlsroutes","tcproutes"]
    verbs: ["get","watch","list"]
---
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: external-dns

3. Configuration Patterns for Cluster Sources

ExternalDNS parses defined resources to extract the hostnames and target endpoints (such as public IPs or LoadBalancer hostnames) to program your DNS records.

A. Exposing Services (Type: LoadBalancer)

For simple services exposed directly via a cloud controller manager’s network load balancer, use annotations to instruct ExternalDNS:

apiVersion: v1
kind: Service
metadata:
  name: web-frontend
  namespace: production
  annotations:
    # Explicitly define target hostnames
    external-dns.alpha.kubernetes.io/hostname: frontend.example.com, web.example.com
    # Set explicit Time-To-Live (TTL) for these records (in seconds)
    external-dns.alpha.kubernetes.io/ttl: "300"
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  selector:
    app: frontend

B. Exposing Ingresses (Nginx / ALB / Traefik)

ExternalDNS automatically tracks the spec.rules[*].host definitions inside Ingresses. It will associate these hostnames with the entry points (IP or Hostname) provisioned in the Ingress’s status.loadBalancer.ingress block.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: production
  annotations:
    kubernetes.io/ingress.class: "nginx"
    # Optional override if you want to route to a different target (e.g., global traffic manager)
    # external-dns.alpha.kubernetes.io/target: "gtm.example.com"
spec:
  rules:
    - host: api.example.com # ExternalDNS watches this host entry
      http:
        paths:
          - path: /v1
            pathType: Prefix
            backend:
              service:
                name: api-v1-service
                port:
                  number: 80

C. Exposing Modern Gateway API Routes

Under the modern Gateway API specification, infrastructure separation isolates Gateway definition (Infra team) from Route definition (App developers). ExternalDNS seamlessly hooks into HTTPRoute resources.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: store-route
  namespace: production
spec:
  parentRefs:
    - name: shared-gateway
      namespace: infra-system
  hostnames:
    - "store.example.com" # ExternalDNS creates DNS pointing to shared-gateway's IP
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /products
      backendRefs:
        - name: store-backend
          port: 8080

Note: When deploying ExternalDNS with Gateway API, configure arguments to monitor route sources:

args:
  - --source=gateway-httproute
  - --source=gateway-grpcroute

4. Multi-Cloud Provider Configurations

ExternalDNS abstracts the backend engine, but setup differences exist for authentication and deployment values.

AWS Route 53 Setup

The secure standard in AWS uses IAM Roles for Service Accounts (IRSA) to assign permissions to the pod without long-lived static keys.

1. Minimum IAM Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "route53:ChangeResourceRecordSets"
      ],
      "Resource": [
        "arn:aws:route53:::hostedzone/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "route53:ListHostedZones",
        "route53:ListResourceRecordSets",
        "route53:ListTagsForResource"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

2. Helm Configuration (values.yaml)

provider: aws
aws:
  region: us-east-1
# Link ServiceAccount to the AWS IAM Role
serviceAccount:
  create: true
  name: external-dns
  annotations:
    [eks.amazonaws.com/role-arn](https://eks.amazonaws.com/role-arn): "arn:aws:iam::112233445566:role/ExternalDNSRole"
domainFilters:
  - example.com
policy: sync # "sync" performs creates, updates, and deletes. "upsert-only" prevents deletions.
txtOwnerId: "eks-production-cluster-01"
sources:
  - service
  - ingress

Cloudflare Integration

Cloudflare API integration uses scoped API tokens restricted to specific Zone permissions.

1. Kubernetes Secret Definition

apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token
  namespace: external-dns
type: Opaque
stringData:
  cloudflare_api_token: "CLOUDFLARE_API_TOKEN_WITH_ZONE_DNS_EDIT"

2. Helm Configuration (values.yaml)

provider: cloudflare
env:
  - name: CF_API_TOKEN
    valueFrom:
      secretKeyRef:
        name: cloudflare-api-token
        key: cloudflare_api_token
cloudflare:
  proxied: false # Toggle whether ExternalDNS sets up CF Proxy (Orange Cloud) for records
domainFilters:
  - example.com
policy: sync
txtOwnerId: "production-k8s-cloudflare"
sources:
  - service
  - ingress

Google Cloud DNS Configuration

Google Cloud workloads secure connections via Workload Identity, linking Kubernetes service accounts directly to GCP Service Accounts.

1. Create GCP Service Account and Bind Permissions

# Create GCP SA
gcloud iam service-accounts create external-dns-sa \
    --display-name "ExternalDNS Service Account"

# Bind DNS Admin role inside GCP Project
gcloud projects add-iam-policy-binding your-gcp-project-id \
    --member "serviceAccount:external-dns-sa@your-gcp-project-id.iam.gserviceaccount.com" \
    --role "roles/dns.admin"

# Link GCP Service Account with K8s Service Account
gcloud iam service-accounts add-iam-policy-binding \
    external-dns-sa@your-gcp-project-id.iam.gserviceaccount.com \
    --role "roles/iam.workloadIdentityUser" \
    --member "serviceAccount:your-gcp-project-id.svc.id.goog[external-dns/external-dns]"

2. Helm Configuration (values.yaml)

provider: google
google:
  project: "your-gcp-project-id"
serviceAccount:
  create: true
  name: external-dns
  annotations:
    iam.gke.io/gcp-service-account: "external-dns-sa@your-gcp-project-id.iam.gserviceaccount.com"
domainFilters:
  - example.com
policy: sync
txtOwnerId: "gke-production-cluster"

5. Security & Production Hardening Best Practices

Use Dry-Run Execution First

Before permitting ExternalDNS to process API modifications against public DNS zones, enable dry-run to monitor updates within your container log.

extraArgs:
  - --dry-run=true

Monitor the controller log using kubectl logs:

INFO[0001] Would create A record named 'app.example.com' with target '192.0.2.1'
INFO[0001] Would create TXT record named 'app.example.com' with text '"heritage=external-dns,external-dns/owner=prod-cluster-01"'

Overwrite Protection (The Multi-Tenant Guardrail)

Always configure the --txt-owner-id argument to match the exact cluster lifecycle tag.

# Scenario: Cluster A and Cluster B manage the same domain space.
Cluster A: --txt-owner-id=k8s-cluster-A
Cluster B: --txt-owner-id=k8s-cluster-B

Without unique Owner IDs, Cluster B will identify DNS records created by Cluster A as “orphaned” and delete or overwrite them based on its synchronization loop.

Strict Domain Filtering

Isolate zones by enforcing explicit domain filters on the controller container. This prevents accidental namespace deployments from modifying DNS records outside your control boundaries.

# Only allowed to create DNS updates on these specified subdomains
domainFilters:
  - app.prod.example.com
  - internal.example.com

Rate Limit Mitigation

Cloud API providers enforce strict request-per-second ceilings on DNS management endpoints.

  1. Configure local DNS caching: Utilize CoreDNS caching loops in-cluster to prevent repeated external lookups.
  2. Increase the synchronization interval: The default sync loop runs every 1 minute. For stable setups, increase this to reduce API consumption overhead.
    extraArgs:
      - --interval=5m
  3. Use TXT prefixes: Use a prefix for ownership TXT records to keep the parent namespace uncluttered.
    extraArgs:
      - --txt-prefix=_externaldns.
    (Creates: _externaldns.app.example.com instead of polluting app.example.com directly).