External DNS#
Automate DNS record management with ExternalDNS for Kubernetes services.
Overview#
ExternalDNS automatically manages DNS records based on Kubernetes Ingress resources. When you create an Ingress with a hostname, ExternalDNS creates the corresponding DNS record in your DNS provider.
Benefits:
- Automatic DNS provisioning for new services
- No manual DNS management
- Keeps DNS records in sync with cluster state
- Supports multiple DNS providers
Supported DNS Providers#
ExternalDNS supports many DNS providers including:
- Cloudflare
- AWS Route53
- Google Cloud DNS
- Azure DNS
- And many more
This documentation focuses on Cloudflare configuration.
Prerequisites#
Cloudflare Setup#
- Cloudflare Account: You need a Cloudflare account with a domain
- API Token: Create an API token with the following permissions:
- Zone - Zone - Read
- Zone - DNS - Edit
- Access to all zones (or specific zones)
To create an API token:
- Log in to Cloudflare Dashboard
- Go to “My Profile” → “API Tokens”
- Click “Create Token”
- Use the “Edit zone DNS” template
- Set appropriate zone resources
- Copy the token (you won’t see it again!)
Installation#
Create API Token Secret#
First, create a Kubernetes secret with your Cloudflare API token:
# Set your API token
export CF_API_TOKEN="your-cloudflare-api-token-here"
# Create the secret
kubectl create secret generic cloudflare-api-token \
--from-literal=cloudflare_api_token=$CF_API_TOKEN \
-n external-dnsOr use the provided script:
# Edit the script with your token
bash external-dns/set-cf-secret.shInstall ExternalDNS using Helm#
Create a values.yaml file for ExternalDNS configuration:
provider: cloudflare
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: cloudflare_api_token
# Only manage DNS for specific domain(s)
domainFilters:
- carlboettiger.info
# Dry-run mode for testing (set to false for production)
dryRun: false
# Log level
logLevel: info
# Registry for tracking ownership
registry: txt
txtOwnerId: k3s-cluster
# Sync policy
policy: sync
# Sources to monitor
sources:
- ingress
- serviceInstall ExternalDNS:
# Add ExternalDNS Helm repository
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm repo update
# Install ExternalDNS
helm install external-dns external-dns/external-dns \
-n external-dns \
--create-namespace \
-f external-dns/values.yamlOr use the provided script:
bash external-dns/helm.shVerify Installation#
# Check ExternalDNS pod is running
kubectl get pods -n external-dns
# View logs
kubectl logs -n external-dns deployment/external-dnsUsage#
Automatic DNS for Ingress Resources#
Simply create an Ingress with a hostname in your managed domain:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
rules:
- host: myapp.carlboettiger.info
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
tls:
- hosts:
- myapp.carlboettiger.info
secretName: myapp-tlsExternalDNS will automatically:
- Detect the new Ingress
- Create a DNS A record for
myapp.carlboettiger.info - Point it to your cluster’s external IP
- Create a TXT record for ownership tracking
Example: JupyterHub#
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- hub.carlboettiger.infoExternalDNS automatically creates the DNS record for hub.carlboettiger.info.
Example: Shiny App#
See examples/shiny/ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shiny-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
external-dns.alpha.kubernetes.io/hostname: shiny.carlboettiger.info
spec:
rules:
- host: shiny.carlboettiger.info
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: shiny-service
port:
number: 3838Custom Annotations#
Control ExternalDNS behavior with annotations:
metadata:
annotations:
# Specify hostname explicitly
external-dns.alpha.kubernetes.io/hostname: myapp.example.com
# Set TTL
external-dns.alpha.kubernetes.io/ttl: "300"
# Exclude from ExternalDNS
external-dns.alpha.kubernetes.io/exclude: "true"Monitoring#
Check DNS Records Created#
# View ExternalDNS logs
kubectl logs -n external-dns deployment/external-dns
# Check for DNS creation events
kubectl logs -n external-dns deployment/external-dns | grep "CREATE"Verify in Cloudflare#
- Log in to Cloudflare Dashboard
- Select your domain
- Go to DNS → Records
- Look for A records created by ExternalDNS
- You should also see TXT records for ownership tracking
Test DNS Resolution#
# Check DNS resolution
nslookup myapp.carlboettiger.info
# Or use dig
dig myapp.carlboettiger.infoTesting#
Test Deployment#
Deploy the test NGINX service:
kubectl apply -f external-dns/test-nginx-deploy.yamlThis creates:
- A Deployment running NGINX
- A Service exposing the deployment
- An Ingress with a test hostname
Check the logs to see ExternalDNS creating the DNS record:
kubectl logs -n external-dns deployment/external-dns -fClean up:
kubectl delete -f external-dns/test-nginx-deploy.yamlTroubleshooting#
DNS Records Not Created#
- Check ExternalDNS logs:
kubectl logs -n external-dns deployment/external-dns- Verify API token:
kubectl get secret cloudflare-api-token -n external-dns -o yamlCheck domain filter: Ensure your Ingress hostname matches the domain filter in
values.yamlVerify permissions:
- Token has Zone Read and DNS Edit permissions
- Token has access to the relevant zone
DNS Records Not Updating#
Check sync interval: ExternalDNS syncs every minute by default
Verify ownership: Check TXT records in Cloudflare
- Format:
txt-<record-name> - Value should match
txtOwnerIdin values.yaml
- Format:
Force sync:
kubectl rollout restart deployment/external-dns -n external-dnsMultiple DNS Records#
If you see duplicate records:
- Check for multiple ExternalDNS deployments
- Verify
txtOwnerIdis unique per cluster - Clean up orphaned TXT records in Cloudflare
Rate Limiting#
Cloudflare has API rate limits:
- If you hit limits, increase sync interval
- Use
--cloudflare-proxied=falseto reduce API calls
Configuration Options#
Dry-Run Mode#
Test without making changes:
dryRun: trueExternalDNS will log what it would do without actually creating/updating DNS records.
Domain Filters#
Restrict to specific domains:
domainFilters:
- example.com
- test.comExclude Domains#
Exclude specific domains:
excludeDomains:
- internal.example.comTXT Record Ownership#
Track DNS record ownership:
registry: txt
txtOwnerId: my-cluster-name
txtPrefix: "external-dns-"Cloudflare Proxy#
Enable Cloudflare CDN proxy (orange cloud):
extraArgs:
- --cloudflare-proxiedNote: When proxied, DNS returns Cloudflare IPs, which prevents direct access to port 6443 for kubectl.
Security Considerations#
- API Token Scope: Use minimum required permissions
- Secret Management: Protect the API token secret
- Ownership Tracking: Use TXT records to prevent conflicts
- Domain Filters: Restrict to specific domains
- Read-Only Mode: Use
policy: upsert-onlyto prevent deletions
Best Practices#
- Test in Dry-Run: Always test with
dryRun: truefirst - Use Domain Filters: Limit ExternalDNS to specific domains
- Monitor Logs: Regularly check ExternalDNS logs
- TXT Record Tracking: Enable ownership tracking
- Backup DNS: Keep manual DNS records backed up
- Multiple Clusters: Use unique
txtOwnerIdper cluster
Integration#
With cert-manager#
ExternalDNS works seamlessly with cert-manager:
- ExternalDNS creates the DNS record
- cert-manager requests the certificate
- Let’s Encrypt verifies via HTTP-01 challenge
- Certificate is issued and stored
With Traefik#
K3s’s built-in Traefik ingress controller works automatically with ExternalDNS:
- Create Ingress with hostname
- Traefik routes traffic
- ExternalDNS creates DNS record
- Traffic flows to your service