Set up External DNS controller to automatically create and manage DNS records for your Kubernetes services and ingresses. This tutorial covers installation, cloud provider integration, and security configuration for production-ready DNS automation.
Prerequisites
- Running Kubernetes cluster with admin access
- Cloud provider account (AWS, GCP, or Azure)
- kubectl and Helm 3 installed
- Domain with cloud-managed DNS zones
What this solves
External DNS automatically creates and manages DNS records for your Kubernetes services and ingresses without manual intervention. When you deploy applications with ingress controllers or LoadBalancer services, External DNS watches these resources and automatically creates corresponding DNS entries in your cloud provider's DNS service, eliminating the need for manual DNS management and reducing deployment overhead.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest package information.
sudo apt update && sudo apt upgrade -y
Install kubectl and Helm
External DNS is best deployed using Helm charts. Install kubectl for cluster access and Helm for package management.
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Create External DNS namespace
Create a dedicated namespace for External DNS to organize resources and apply security policies.
kubectl create namespace external-dns
Create service account and RBAC
External DNS needs cluster-wide permissions to watch ingresses, services, and create DNS records. Create the necessary RBAC configuration.
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: external-dns
---
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: external-dns
Apply RBAC configuration
Apply the RBAC configuration to grant External DNS the necessary permissions.
kubectl apply -f external-dns-rbac.yaml
Configure cloud provider credentials (AWS example)
Create AWS credentials for External DNS to manage Route53 records. Replace with your actual access keys.
apiVersion: v1
kind: Secret
metadata:
name: aws-credentials
namespace: external-dns
type: Opaque
data:
access-key-id: QUtJQVlPVVJBQ0NFU1NLRVK=
secret-access-key: eW91ci1zZWNyZXQtYWNjZXNzLWtle=
Apply credentials secret
Apply the credentials secret to enable External DNS authentication with your cloud provider.
kubectl apply -f aws-credentials-secret.yaml
Deploy External DNS with Helm
Add the External DNS Helm repository and install the controller with AWS Route53 configuration.
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm repo update
Create Helm values file
Configure External DNS for your cloud provider and domain. This example uses AWS Route53.
provider: aws
aws:
zoneType: public
assumeRoleArn: ""
batchChangeSize: 1000
evaluateTargetHealth: true
domainFilters:
- example.com
zoneIdFilters: []
policy: upsert-only
registry: txt
txtOwnerId: external-dns
txtPrefix: external-dns-
interval: 1m
triggerLoopOnEvent: false
logLevel: info
logFormat: text
metrics:
enabled: true
port: 7979
serviceAccount:
create: false
name: external-dns
securityContext:
fsGroup: 65534
runAsUser: 65534
runAsNonRoot: true
resources:
limits:
cpu: 250m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: aws-credentials
key: secret-access-key
Install External DNS
Deploy External DNS using the Helm chart with your custom values.
helm install external-dns external-dns/external-dns \
--namespace external-dns \
--values external-dns-values.yaml
Configure for Google Cloud DNS (alternative)
If using Google Cloud DNS instead of AWS, create a service account key and configure different values.
provider: google
google:
project: your-gcp-project-id
serviceAccountSecretKey: credentials.json
domainFilters:
- example.com
policy: upsert-only
registry: txt
txtOwnerId: external-dns
interval: 1m
serviceAccount:
create: false
name: external-dns
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /etc/secrets/service-account/credentials.json
extraVolumes:
- name: google-service-account
secret:
secretName: external-dns-gcp-sa
extraVolumeMounts:
- name: google-service-account
mountPath: /etc/secrets/service-account
readOnly: true
Configure network policies (optional)
Implement network policies to restrict External DNS network access for enhanced security.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: external-dns-network-policy
namespace: external-dns
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: external-dns
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- protocol: TCP
port: 7979
egress:
- to: []
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
- protocol: TCP
port: 443
Apply network policies
Apply the network policies to secure External DNS network communications.
kubectl apply -f external-dns-network-policy.yaml
Test with sample ingress
Create a test ingress to verify External DNS automatically creates DNS records.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-app
namespace: default
annotations:
kubernetes.io/ingress.class: nginx
external-dns.alpha.kubernetes.io/hostname: test.example.com
spec:
tls:
- hosts:
- test.example.com
secretName: test-app-tls
rules:
- host: test.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-service
port:
number: 80
Apply test ingress
Deploy the test ingress and watch External DNS create the corresponding DNS record.
kubectl apply -f test-ingress.yaml
Verify your setup
Check that External DNS is running correctly and processing DNS records.
kubectl get pods -n external-dns
kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns
kubectl get ingress -A
nslookup test.example.com
Monitor External DNS metrics and verify DNS record creation.
kubectl port-forward -n external-dns svc/external-dns 7979:7979
curl http://localhost:7979/metrics
dig test.example.com
Configure for production environments
Enable monitoring integration
Configure External DNS to work with your existing monitoring stack. This integrates with Kubernetes monitoring with Prometheus and Grafana.
metrics:
enabled: true
port: 7979
serviceMonitor:
enabled: true
namespace: monitoring
interval: 30s
scrapeTimeout: 10s
labels:
prometheus: kube-prometheus
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "7979"
prometheus.io/path: "/metrics"
Configure resource limits and requests
Set appropriate resource limits for production workloads to ensure stability.
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
podDisruptionBudget:
enabled: true
minAvailable: 1
replicaCount: 2
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- external-dns
topologyKey: kubernetes.io/hostname
Configure DNS zone filters
Limit External DNS to specific zones for security and prevent accidental DNS modifications.
domainFilters:
- example.com
- staging.example.com
zoneIdFilters:
- Z1D633PJN98FT9 # production zone
- Z2E744QKM87GH2 # staging zone
policy: sync
annotationFilter: external-dns.alpha.kubernetes.io/exclude notin (true)
txtPrefix: k8s-
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| DNS records not created | Missing RBAC permissions | Verify service account has cluster-wide ingress/service read permissions |
| Access denied errors | Invalid cloud credentials | Check AWS credentials or IAM role permissions for Route53 |
| External DNS pod crash looping | Invalid domain filters or zone configuration | Check logs with kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns |
| DNS records created but not resolving | TTL propagation delay | Wait 5-10 minutes for DNS propagation, check with dig +trace hostname |
| Multiple DNS records for same hostname | Conflicting ingress annotations | Use external-dns.alpha.kubernetes.io/hostname annotation consistently |
| External DNS not watching ingresses | Incorrect annotation filter | Remove or adjust annotation filters in External DNS configuration |
Security best practices
For production deployments, consider implementing Pod Security Standards and admission controllers to enforce security policies across your cluster.
Create minimal IAM policy for AWS
Create a restrictive IAM policy that only allows DNS operations on specific hosted zones.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/Z1D633PJN98FT9"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets"
],
"Resource": [
"*"
]
}
]
}
Next steps
- Setup Kubernetes Ingress NGINX with cert-manager for automated SSL certificates
- Configure Kubernetes network policies with Calico CNI for microsegmentation
- Configure External DNS with Cloudflare integration
- Setup External DNS with Azure DNS integration
- Implement External DNS multi-cluster management
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Global variables
DOMAIN=""
CLOUD_PROVIDER="aws"
CLEANUP_DONE=false
# Print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Usage message
usage() {
cat << EOF
Usage: $0 -d DOMAIN [-p PROVIDER]
Install and configure Kubernetes External DNS for automatic DNS record management
Options:
-d DOMAIN Domain name to manage (required, e.g., example.com)
-p PROVIDER Cloud provider (default: aws, supports: aws, gcp, azure)
-h Show this help message
Examples:
$0 -d example.com
$0 -d mysite.com -p aws
EOF
exit 1
}
# Parse command line arguments
while getopts "d:p:h" opt; do
case $opt in
d) DOMAIN="$OPTARG" ;;
p) CLOUD_PROVIDER="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
# Validate required arguments
if [ -z "$DOMAIN" ]; then
print_error "Domain name is required"
usage
fi
# Cleanup function for rollback
cleanup() {
if [ "$CLEANUP_DONE" = false ]; then
print_warning "Script failed, cleaning up..."
kubectl delete namespace external-dns --ignore-not-found=true 2>/dev/null || true
helm repo remove external-dns 2>/dev/null || true
rm -f /tmp/external-dns-rbac.yaml /tmp/external-dns-values.yaml
CLEANUP_DONE=true
fi
}
# Set trap for cleanup on error
trap cleanup ERR
# Detect Linux distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update -y"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
;;
*)
print_error "Unsupported distribution: $ID"
exit 1
;;
esac
export PKG_MGR PKG_UPDATE PKG_INSTALL
else
print_error "Cannot detect Linux distribution"
exit 1
fi
}
# Check prerequisites
check_prerequisites() {
print_status "[1/8] Checking prerequisites..."
if [ "$EUID" -eq 0 ]; then
print_error "Do not run this script as root. Run as a user with sudo privileges."
exit 1
fi
if ! command -v sudo >/dev/null 2>&1; then
print_error "sudo is required but not installed"
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
print_error "curl is required but not installed"
exit 1
fi
}
# Update system packages
update_system() {
print_status "[2/8] Updating system packages..."
sudo $PKG_UPDATE
}
# Install kubectl and helm
install_tools() {
print_status "[3/8] Installing kubectl and Helm..."
# Install kubectl
if ! command -v kubectl >/dev/null 2>&1; then
KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
rm -f kubectl
else
print_warning "kubectl already installed, skipping..."
fi
# Install Helm
if ! command -v helm >/dev/null 2>&1; then
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
else
print_warning "helm already installed, skipping..."
fi
}
# Create namespace
create_namespace() {
print_status "[4/8] Creating External DNS namespace..."
kubectl create namespace external-dns --dry-run=client -o yaml | kubectl apply -f -
}
# Create RBAC configuration
create_rbac() {
print_status "[5/8] Creating RBAC configuration..."
cat > /tmp/external-dns-rbac.yaml << 'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: external-dns
---
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: external-dns
EOF
kubectl apply -f /tmp/external-dns-rbac.yaml
}
# Add Helm repository
add_helm_repo() {
print_status "[6/8] Adding External DNS Helm repository..."
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm repo update
}
# Create Helm values file
create_values_file() {
print_status "[7/8] Creating Helm values configuration..."
case "$CLOUD_PROVIDER" in
aws)
cat > /tmp/external-dns-values.yaml << EOF
provider: aws
aws:
zoneType: public
batchChangeSize: 1000
evaluateTargetHealth: true
domainFilters:
- ${DOMAIN}
policy: upsert-only
registry: txt
txtOwnerId: external-dns-${DOMAIN}
txtPrefix: external-dns-
interval: 1m
triggerLoopOnEvent: false
logLevel: info
logFormat: text
metrics:
enabled: true
port: 7979
serviceAccount:
create: false
name: external-dns
resources:
limits:
cpu: 50m
memory: 50Mi
requests:
cpu: 10m
memory: 50Mi
securityContext:
fsGroup: 65534
runAsUser: 65534
runAsNonRoot: true
EOF
;;
gcp)
cat > /tmp/external-dns-values.yaml << EOF
provider: google
google:
project: ""
domainFilters:
- ${DOMAIN}
policy: upsert-only
registry: txt
txtOwnerId: external-dns-${DOMAIN}
serviceAccount:
create: false
name: external-dns
EOF
;;
*)
print_error "Unsupported cloud provider: $CLOUD_PROVIDER"
exit 1
;;
esac
}
# Deploy External DNS
deploy_external_dns() {
print_status "[8/8] Deploying External DNS..."
helm upgrade --install external-dns external-dns/external-dns \
--namespace external-dns \
--values /tmp/external-dns-values.yaml \
--wait --timeout=300s
}
# Verify installation
verify_installation() {
print_status "Verifying External DNS installation..."
# Check if deployment is ready
kubectl wait --for=condition=available --timeout=300s deployment/external-dns -n external-dns
# Check pod status
kubectl get pods -n external-dns
# Check logs for any immediate errors
print_status "Checking External DNS logs..."
kubectl logs -n external-dns deployment/external-dns --tail=10
print_status "External DNS installation completed successfully!"
echo ""
print_warning "Next steps:"
echo "1. Configure cloud provider credentials (AWS IAM role, GCP service account, etc.)"
echo "2. Create an ingress or LoadBalancer service with external-dns annotations"
echo "3. Verify DNS records are created in your cloud provider's DNS service"
echo ""
echo "Example ingress annotation:"
echo " external-dns.alpha.kubernetes.io/hostname: app.${DOMAIN}"
}
# Cleanup temporary files
cleanup_files() {
rm -f /tmp/external-dns-rbac.yaml /tmp/external-dns-values.yaml
CLEANUP_DONE=true
}
# Main execution
main() {
print_status "Starting External DNS installation for domain: $DOMAIN"
detect_distro
check_prerequisites
update_system
install_tools
create_namespace
create_rbac
add_helm_repo
create_values_file
deploy_external_dns
verify_installation
cleanup_files
print_status "External DNS installation completed successfully!"
}
main "$@"
Review the script before running. Execute with: bash install.sh