ExternalDNS Integration Guide β
This guide shows how to integrate Lynq with ExternalDNS for automatic DNS record management.
Overview β
ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers like AWS Route53, Google Cloud DNS, Cloudflare, and more. When integrated with Lynq, each node's DNS records are automatically created and deleted as nodes are provisioned.
Use Cases β
- Multi-node SaaS: Automatic subdomain creation per node (e.g.,
node-a.example.com,node-b.example.com) - Dynamic environments: DNS records follow node lifecycle (created/deleted with node)
- Multiple domains: Different nodes on different domains or subdomains
- SSL/TLS automation: Combined with cert-manager for automatic certificate provisioning
Prerequisites β
Requirements
- Kubernetes cluster v1.11+
- Lynq installed and reconciling
- DNS provider account (AWS Route53, Cloudflare, etc.)
- DNS zone created in your provider
Installation β
1. Install ExternalDNS β
Using Helm (Recommended) β
# Add bitnami repo
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Install ExternalDNS for AWS Route53
helm install external-dns bitnami/external-dns \
--namespace kube-system \
--set provider=aws \
--set aws.zoneType=public \
--set domainFilters[0]=example.com \
--set policy=upsert-only \
--set txtOwnerId=my-cluster-id
# Or for Cloudflare
helm install external-dns bitnami/external-dns \
--namespace kube-system \
--set provider=cloudflare \
--set cloudflare.apiToken=<your-api-token> \
--set domainFilters[0]=example.comUsing Manifests β
For detailed YAML manifests and provider-specific configurations, see:
2. Verify Installation β
# Check ExternalDNS pod
kubectl get pods -n kube-system -l app.kubernetes.io/name=external-dns
# Check logs
kubectl logs -n kube-system -l app.kubernetes.io/name=external-dnsIntegration with Lynq β
Basic Example: Ingress with Automatic DNS β
LynqForm with ExternalDNS annotations:
apiVersion: operator.lynq.sh/v1
kind: LynqForm
metadata:
name: web-app-with-dns
namespace: default
spec:
hubId: my-hub
# Deployment
deployments:
- id: app
nameTemplate: "{{ .uid }}-app"
spec:
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 2
selector:
matchLabels:
app: "{{ .uid }}"
template:
metadata:
labels:
app: "{{ .uid }}"
spec:
containers:
- name: app
image: nginx:alpine
ports:
- containerPort: 80
# Service
services:
- id: app-service
nameTemplate: "{{ .uid }}-svc"
spec:
apiVersion: v1
kind: Service
spec:
selector:
app: "{{ .uid }}"
ports:
- port: 80
targetPort: 80
# Ingress with ExternalDNS annotation
ingresses:
- id: web-ingress
nameTemplate: "{{ .uid }}-ingress"
annotationsTemplate:
external-dns.alpha.kubernetes.io/hostname: "{{ .host }}"
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
ingressClassName: nginx
rules:
- host: "{{ .host }}"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: "{{ .uid }}-svc"
port:
number: 80What happens:
- Lynq creates Ingress for each node (e.g.,
acme-corp-ingress) - ExternalDNS detects Ingress with
external-dns.alpha.kubernetes.io/hostnameannotation - ExternalDNS creates DNS A/AAAA record pointing to Ingress LoadBalancer IP
- When node is deleted, DNS record is automatically removed
Result: Each node gets automatic DNS:
acme-corp.example.comβ 1.2.3.4beta-inc.example.comβ 1.2.3.4
LoadBalancer Service Example β
For LoadBalancer Services (instead of Ingress):
services:
- id: lb-service
nameTemplate: "{{ .uid }}-lb"
annotationsTemplate:
external-dns.alpha.kubernetes.io/hostname: "{{ .host }}"
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
apiVersion: v1
kind: Service
type: LoadBalancer
spec:
selector:
app: "{{ .uid }}"
ports:
- port: 80
targetPort: 80How It Works β
Workflow β
- Node Created: LynqHub creates LynqNode CR from database
- Resources Applied: LynqNode controller creates Ingress/Service with ExternalDNS annotations
- IP Assignment: Kubernetes assigns LoadBalancer IP or Ingress IP
- DNS Sync: ExternalDNS detects annotated resource and creates DNS record
- Propagation: DNS record propagates through provider (seconds to minutes)
- Node Deleted: LynqNode resources deleted β ExternalDNS removes DNS record
DNS Record Lifecycle β
Common Annotations β
Required β
| Annotation | Description | Example |
|---|---|---|
external-dns.alpha.kubernetes.io/hostname | DNS hostname to create | node.example.com |
Optional β
| Annotation | Description | Default | Example |
|---|---|---|---|
external-dns.alpha.kubernetes.io/ttl | DNS TTL in seconds | 300 | 600 |
external-dns.alpha.kubernetes.io/target | Override target IP/CNAME | Auto-detected | 1.2.3.4 |
external-dns.alpha.kubernetes.io/alias | Use DNS alias (AWS Route53) | false | true |
Provider-Specific β
AWS Route53:
annotations:
external-dns.alpha.kubernetes.io/aws-weight: "100"
external-dns.alpha.kubernetes.io/set-identifier: "primary"Cloudflare:
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"Multi-Domain Example β
Support different domains per node using template variables:
apiVersion: operator.lynq.sh/v1
kind: LynqForm
metadata:
name: multi-domain-template
spec:
hubId: my-hub
ingresses:
- id: node-ingress
nameTemplate: "{{ .uid }}-ingress"
annotationsTemplate:
# Use .host which is auto-extracted from .hostOrUrl
external-dns.alpha.kubernetes.io/hostname: "{{ .host }}"
spec:
apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
ingressClassName: nginx
rules:
- host: "{{ .host }}"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: "{{ .uid }}-svc"
port:
number: 80Database rows:
node_id node_url is_active
--------- -------------------------- ---------
acme-corp https://acme.example.com 1
beta-inc https://beta.example.io 1
gamma-co https://custom.domain.net 1Result:
acme.example.comβ acme-corp nodebeta.example.ioβ beta-inc nodecustom.domain.netβ gamma-co node
Troubleshooting β
DNS Records Not Created β
Problem: DNS records don't appear in provider.
Solution:
Check ExternalDNS logs:
bashkubectl logs -n kube-system -l app.kubernetes.io/name=external-dnsVerify Ingress has IP:
bashkubectl get ingress <lynqnode-ingress> -o jsonpath='{.status.loadBalancer.ingress[0].ip}'Check annotation syntax:
bashkubectl get ingress <lynqnode-ingress> -o yaml | grep external-dnsVerify domain filter:
bashkubectl get deployment external-dns -n kube-system -o yaml | grep domain-filter
DNS Records Not Deleted β
Problem: DNS records remain after node deletion.
Solution:
Check ExternalDNS policy:
policy=upsert-onlyprevents deletion (change topolicy=sync)policy=syncallows ExternalDNS to delete records
Check TXT records:
bashdig TXT <node-domain>TXT records track ownership - if owner doesn't match, record won't be deleted.
DNS Propagation Delays β
Problem: DNS changes take too long to propagate.
Solution:
Reduce TTL:
yamlannotations: external-dns.alpha.kubernetes.io/ttl: "60" # 1 minuteCheck DNS propagation:
bashdig <node-domain> @8.8.8.8 dig <node-domain> @1.1.1.1Use DNS checker:
Best Practices β
1. Use Separate Hosted Zones β
Use dedicated DNS zones for node subdomains:
# Production nodes
--domain-filter=example.com
# Staging nodes
--domain-filter=staging.example.com2. Set Appropriate TTLs β
annotations:
external-dns.alpha.kubernetes.io/ttl: "300" # 5 minutes (good for production)
# external-dns.alpha.kubernetes.io/ttl: "60" # 1 minute (good for testing)3. Use Policy: upsert-only for Safety β
Prevent ExternalDNS from deleting existing records:
helm install external-dns bitnami/external-dns \
--set policy=upsert-only4. Monitor ExternalDNS Logs β
kubectl logs -n kube-system -l app.kubernetes.io/name=external-dns -f5. Combine with cert-manager β
Auto-provision SSL certificates with DNS challenge:
ingresses:
- id: secure-ingress
nameTemplate: "{{ .uid }}-ingress"
annotationsTemplate:
external-dns.alpha.kubernetes.io/hostname: "{{ .host }}"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
tls:
- hosts:
- "{{ .host }}"
secretName: "{{ .uid }}-tls"
rules:
- host: "{{ .host }}"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: "{{ .uid }}-svc"
port:
number: 80