Skill v1.0.1
currentLLM-judged scan95/100+1 new
version: "1.0.1" name: external-dns description: Comprehensive guide for configuring, troubleshooting, and implementing External-DNS across Azure DNS, AWS Route53, Cloudflare, and Google Cloud DNS. Use when implementing automatic DNS management in Kubernetes, configuring provider-specific authentication (managed identities, IRSA, API tokens), troubleshooting DNS synchronization issues, setting up secure production-grade external-dns deployments, optimizing performance, avoiding rate limits, or implementing GitOps patterns with ArgoCD.
External-DNS Skill
Complete External-DNS operations for automatic DNS management in Kubernetes clusters.
Overview
External-DNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers, eliminating manual DNS record management. This skill covers configuration, best practices, and troubleshooting across multiple DNS providers with emphasis on Azure and Cloudflare.
Provider Quick Reference
| Provider | Auth Method | Status | Reference | |
|---|---|---|---|---|
| Azure DNS | Workload Identity (recommended) or Service Principal | Stable | references/azure-dns.md | |
| Cloudflare | API Token | Beta | references/cloudflare.md | |
| AWS Route53 | IRSA (recommended) or Access Keys | Stable | Below | |
| Google Cloud DNS | Workload Identity | Stable | Below |
Essential Helm Values Structure
# kubernetes-sigs/external-dns chart (v1.18.0+)fullnameOverride: external-dnsprovider:name: <provider> # azure, cloudflare, aws, google# Sources to watchsources:- service- ingress# Domain restrictionsdomainFilters:- example.com# Policy: sync (creates/updates/deletes) or upsert-only (creates/updates only)policy: upsert-only # Recommended for production# Sync intervalinterval: "5m"# TXT record ownership (MUST be unique per cluster)txtOwnerId: "aks-cluster-name"txtPrefix: "_externaldns."# LogginglogLevel: info # debug, info, warning, errorlogFormat: json# Resourcesresources:requests:memory: "64Mi"cpu: "25m"limits:memory: "128Mi"# cpu: REMOVED per best practice (no CPU limits)# Security contextsecurityContext:runAsNonRoot: truerunAsUser: 65534allowPrivilegeEscalation: falsereadOnlyRootFilesystem: truecapabilities:drop: ["ALL"]# Prometheus metricsserviceMonitor:enabled: trueinterval: 30s
Azure DNS Configuration
Workload Identity (Recommended)
provider:name: azureserviceAccount:labels:azure.workload.identity/use: "true"annotations:azure.workload.identity/client-id: "<MANAGED_IDENTITY_CLIENT_ID>"podLabels:azure.workload.identity/use: "true"env:- name: AZURE_TENANT_IDvalue: "<TENANT_ID>"- name: AZURE_SUBSCRIPTION_IDvalue: "<SUBSCRIPTION_ID>"- name: AZURE_RESOURCE_GROUPvalue: "<DNS_ZONE_RESOURCE_GROUP>"domainFilters:- example.comtxtOwnerId: "aks-cluster-name"policy: upsert-onlyinterval: "5m"
Required Azure RBAC Permissions
# Assign DNS Zone Contributor role to the managed identityaz role assignment create \--role "DNS Zone Contributor" \--assignee "<MANAGED_IDENTITY_OBJECT_ID>" \--scope "/subscriptions/<SUB_ID>/resourceGroups/<RG>/providers/Microsoft.Network/dnszones/<ZONE>"# For Private DNS Zonesaz role assignment create \--role "Private DNS Zone Contributor" \--assignee "<MANAGED_IDENTITY_OBJECT_ID>" \--scope "/subscriptions/<SUB_ID>/resourceGroups/<RG>/providers/Microsoft.Network/privateDnsZones/<ZONE>"
Service Principal Alternative
provider:name: azureenv:- name: AZURE_TENANT_IDvalue: "<TENANT_ID>"- name: AZURE_SUBSCRIPTION_IDvalue: "<SUBSCRIPTION_ID>"- name: AZURE_RESOURCE_GROUPvalue: "<DNS_ZONE_RESOURCE_GROUP>"- name: AZURE_CLIENT_IDvalueFrom:secretKeyRef:name: azure-credentialskey: client-id- name: AZURE_CLIENT_SECRETvalueFrom:secretKeyRef:name: azure-credentialskey: client-secret
Cloudflare Configuration
provider:name: cloudflareenv:- name: CF_API_TOKENvalueFrom:secretKeyRef:name: cloudflare-api-tokenkey: cloudflare_api_tokenextraArgs:cloudflare-proxied: true # Enable CDN/DDoS protectioncloudflare-dns-records-per-page: 5000 # Optimize API callsdomainFilters:- example.comtxtOwnerId: "aks-cluster-name"policy: upsert-only
Cloudflare API Token Permissions
- Zone:Read - List zones
- DNS:Edit - Create/update/delete DNS records
- Zone Resources: All zones or specific zones
AWS Route53 Configuration (IRSA)
provider:name: awsenv:- name: AWS_DEFAULT_REGIONvalue: "us-east-1"serviceAccount:annotations:eks.amazonaws.com/role-arn: "arn:aws:iam::<ACCOUNT_ID>:role/external-dns"extraArgs:aws-zone-type: public # or privateaws-batch-change-size: 4000domainFilters:- example.comtxtOwnerId: "eks-cluster-name"
Required AWS IAM Policy
{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": ["route53:ChangeResourceRecordSets"],"Resource": ["arn:aws:route53:::hostedzone/*"]},{"Effect": "Allow","Action": ["route53:ListHostedZones", "route53:ListResourceRecordSets"],"Resource": ["*"]}]}
Google Cloud DNS Configuration
provider:name: googleenv:- name: GOOGLE_PROJECTvalue: "<GCP_PROJECT_ID>"serviceAccount:annotations:iam.gke.io/gcp-service-account: "external-dns@<PROJECT_ID>.iam.gserviceaccount.com"domainFilters:- example.comtxtOwnerId: "gke-cluster-name"
Kubernetes Resource Annotations
Basic Usage
# On Service or Ingressmetadata:annotations:external-dns.alpha.kubernetes.io/hostname: "app.example.com"external-dns.alpha.kubernetes.io/ttl: "300"
Multiple Hostnames
metadata:annotations:external-dns.alpha.kubernetes.io/hostname: "app1.example.com,app2.example.com"
Provider-Specific Annotations
# Cloudflare - disable proxy for specific recordexternal-dns.alpha.kubernetes.io/cloudflare-proxied: "false"# AWS Route53 - create ALIAS recordexternal-dns.alpha.kubernetes.io/alias: "true"# Custom TTLexternal-dns.alpha.kubernetes.io/ttl: "60"
Environment-Specific Best Practices
Development
policy: sync # Auto-delete orphaned recordsinterval: "1m" # Fast sync for rapid iterationlogLevel: inforesources:requests:memory: "50Mi"cpu: "10m"limits:memory: "50Mi"
Production
policy: upsert-only # NEVER auto-deleteinterval: "10m" # Conservative to reduce API loadlogLevel: error # Minimal logging# High AvailabilityreplicaCount: 2affinity:podAntiAffinity:requiredDuringSchedulingIgnoredDuringExecution:- labelSelector:matchExpressions:- key: app.kubernetes.io/nameoperator: Invalues: [external-dns]topologyKey: kubernetes.io/hostnamepodDisruptionBudget:enabled: trueminAvailable: 1priorityClassName: high-priority
Common Commands
# Check external-dns podskubectl get pods -n external-dns# View logskubectl logs -n external-dns deployment/external-dns --tail=100 -f# Check configurationkubectl get deployment external-dns -n external-dns -o yaml | grep -A20 args# Verify DNS records (Cloudflare)dig @1.1.1.1 app.example.com# Verify DNS records (Azure)az network dns record-set list -g <RESOURCE_GROUP> -z example.com -o table# Check TXT ownership recordsdig TXT _externaldns.app.example.com# Force restartkubectl rollout restart deployment external-dns -n external-dns# Dry-run mode (add to extraArgs)extraArgs:dry-run: true
Key Metrics
# Total endpoints managedexternal_dns_registry_endpoints_total# Sync errorsexternal_dns_controller_sync_errors_total# Last sync timestampexternal_dns_controller_last_sync_timestamp_seconds# DNS records by typeexternal_dns_registry_a_recordsexternal_dns_registry_aaaa_recordsexternal_dns_registry_cname_records
ArgoCD ApplicationSet Pattern
apiVersion: argoproj.io/v1alpha1kind: ApplicationSetmetadata:name: external-dnsnamespace: argocdspec:generators:- list:elements:- cluster: devbranch: main- cluster: prdbranch: maintemplate:metadata:name: 'external-dns-{{cluster}}'spec:project: infrastructuresources:- chart: external-dnsrepoURL: https://kubernetes-sigs.github.io/external-dns/targetRevision: "1.18.0"helm:releaseName: external-dnsvalueFiles:- $values/argo-cd-helm-values/kube-addons/external-dns/{{cluster}}/values.yaml- repoURL: https://your-repo.gittargetRevision: "{{branch}}"ref: valuesdestination:server: '{{url}}'namespace: external-dnssyncPolicy:automated:prune: trueselfHeal: truesyncOptions:- CreateNamespace=true
Security Checklist
- [ ] Use Workload Identity/IRSA instead of static credentials
- [ ] Grant least privilege permissions to DNS zones
- [ ] Set
runAsNonRoot: trueandreadOnlyRootFilesystem: true - [ ] Use unique
txtOwnerIdper cluster - [ ] Restrict
domainFiltersto necessary domains - [ ] Store API tokens in Kubernetes Secrets
- [ ] Enable Pod Security Standards (restricted)
- [ ] Use
policy: upsert-onlyin production
References
references/azure-dns.md- Complete Azure DNS configuration guidereferences/cloudflare.md- Complete Cloudflare configuration guidereferences/troubleshooting.md- Common issues and solutions- Official docs: <https://kubernetes-sigs.github.io/external-dns/>
- Helm chart: <https://artifacthub.io/packages/helm/external-dns/external-dns>
Gotchas
- `txtOwnerId` collisions silently corrupt DNS across clusters: Two clusters with the same owner ID will reconcile each other's records into oblivion. Always use cluster-name + env (e.g.,
aks-example-app-prd) and verify withdig TXT _externaldns.<host>. - `policy: sync` deletes records External-DNS didn't create when names match patterns: A manually-created A record matching a managed hostname will be deleted on next reconcile. Production must be
upsert-only; only dev clusters getsync. - RBAC on K8s side AND DNS provider creds are both required: External-DNS needs to read Ingress/Service objects AND have DNS-zone write. Read-only DNS creds produce silent no-ops with zero events emitted to the watched resources — only the pod logs show the auth error.
- Workload Identity needs three things, not one: ServiceAccount annotation + pod label + federated credential on the managed identity. Missing the federated credential gives
ManagedIdentityCredential: 400that looks like a token problem but is an identity-binding problem. - `domainFilters` is prefix-matching, not exact:
domainFilters: [example.com]will manageevil-example.comif a hostile Ingress claims that hostname. Use--exclude-domainsor stricter filtering on multi-tenant clusters. - Azure Private DNS Zone needs a different role than public: "DNS Zone Contributor" only works on public zones; private zones need "Private DNS Zone Contributor". Assigning the wrong one returns 403 only when the first record sync fires, not at deploy time.