# Split Horizon DNS Split horizon DNS allows you to serve different DNS responses based on the client's location - internal clients receive private IPs while external clients receive public IPs. External-DNS supports split horizon DNS by running multiple instances with different annotation prefixes. ## Overview By default, all external-dns instances use the same annotation prefix: `external-dns.alpha.kubernetes.io/`. This means all instances process the same annotations. To enable split horizon DNS, you can configure each instance to use a different annotation prefix via the `--annotation-prefix` flag. ## Use Cases - **Internal/External separation**: Internal DNS points to private IPs (ClusterIP), external DNS points to public Load Balancer IPs - **Multiple DNS providers**: Route different services to different DNS providers (e.g., internal to CoreDNS, external to Route53) - **Geographic split**: Different DNS records for different regions ## Configuration ### Basic Split Horizon Setup **Internal DNS Instance:** ```bash external-dns \ --annotation-prefix=internal.company.io/ \ --source=service \ --source=ingress \ --provider=aws \ --aws-zone-type=private \ --domain-filter=internal.company.com \ --txt-owner-id=internal-dns ``` **External DNS Instance:** ```bash external-dns \ --annotation-prefix=external-dns.alpha.kubernetes.io/ \ # default, can be omitted --source=service \ --source=ingress \ --provider=aws \ --aws-zone-type=public \ --domain-filter=company.com \ --txt-owner-id=external-dns ``` ### Service with Both Annotations ```yaml apiVersion: v1 kind: Service metadata: name: myapp annotations: # Internal DNS reads this internal.company.io/hostname: myapp.internal.company.com internal.company.io/ttl: "300" internal.company.io/target: 10.0.1.50 # Private IP # External DNS reads this external-dns.alpha.kubernetes.io/hostname: myapp.company.com external-dns.alpha.kubernetes.io/ttl: "60" # No target = uses LoadBalancer IP automatically spec: type: LoadBalancer clusterIP: 10.0.1.50 ports: - port: 80 targetPort: 8080 selector: app: myapp ``` **Result:** - **Internal DNS** (Route53 Private Zone `internal.company.com`): `myapp.internal.company.com → 10.0.1.50` - **External DNS** (Route53 Public Zone `company.com`): `myapp.company.com → 203.0.113.10` (LoadBalancer IP) ### Helm Chart Configuration You can use the Helm chart to deploy multiple instances: **values-internal.yaml:** ```yaml annotationPrefix: "internal.company.io/" provider: name: aws aws: zoneType: private domainFilters: - internal.company.com txtOwnerId: internal-dns sources: - service - ingress ``` **values-external.yaml:** ```yaml # annotationPrefix defaults to "external-dns.alpha.kubernetes.io/" # can be omitted or set explicitly: # annotationPrefix: "external-dns.alpha.kubernetes.io/" provider: name: aws aws: zoneType: public domainFilters: - company.com txtOwnerId: external-dns sources: - service - ingress ``` **Deploy:** ```bash # Internal instance helm install external-dns-internal external-dns/external-dns \ --namespace external-dns-internal \ --create-namespace \ --values values-internal.yaml # External instance helm install external-dns-external external-dns/external-dns \ --namespace external-dns-external \ --create-namespace \ --values values-external.yaml ``` ## Advanced Examples ### Three-Way Split (Internal / DMZ / External) ```yaml apiVersion: v1 kind: Service metadata: name: api annotations: # Internal (private network only) internal.company.io/hostname: api.internal.company.com internal.company.io/ttl: "300" # DMZ (accessible from office network) dmz.company.io/hostname: api.dmz.company.com dmz.company.io/ttl: "120" # External (public internet) external-dns.alpha.kubernetes.io/hostname: api.company.com external-dns.alpha.kubernetes.io/ttl: "60" external-dns.alpha.kubernetes.io/cloudflare-proxied: "true" spec: type: LoadBalancer # ... ``` **Deploy three instances:** ```bash # Internal --annotation-prefix=internal.company.io/ --provider=aws --aws-zone-type=private # DMZ --annotation-prefix=dmz.company.io/ --provider=aws --aws-zone-type=private # External --annotation-prefix=external-dns.alpha.kubernetes.io/ --provider=cloudflare ``` ### Different Providers Per Instance ```yaml apiVersion: v1 kind: Service metadata: name: webapp annotations: # Route53 for AWS internal aws.company.io/hostname: webapp.aws.company.com aws.company.io/aws-alias: "true" # Cloudflare for public cf.company.io/hostname: webapp.company.com cf.company.io/cloudflare-proxied: "true" spec: type: LoadBalancer # ... ``` **Deploy:** ```bash # AWS instance --annotation-prefix=aws.company.io/ --provider=aws # Cloudflare instance --annotation-prefix=cf.company.io/ --provider=cloudflare ``` ## Important Notes 1. **Annotation prefix must end with `/`** - The validation will fail if the prefix doesn't end with a forward slash. 2. **Backward compatibility** - If you don't specify `--annotation-prefix`, the default `external-dns.alpha.kubernetes.io/` is used, maintaining full backward compatibility. 3. **All annotations use the same prefix** - When you set a custom prefix, ALL external-dns annotations (hostname, ttl, target, cloudflare-proxied, etc.) must use that prefix. 4. **TXT ownership records** - Each instance should have a unique `--txt-owner-id` to avoid conflicts in ownership tracking. 5. **Provider-specific annotations** - Provider-specific annotations (like `cloudflare-proxied`, `aws-alias`) also use the custom prefix: ```yaml custom.io/hostname: example.com custom.io/cloudflare-proxied: "true" # NOT external-dns.alpha.kubernetes.io/cloudflare-proxied ``` ## Troubleshooting ### Both instances processing the same resources **Problem:** Both internal and external instances are creating records for the same service. **Solution:** Make sure you're using different annotation prefixes and that your services have the correct annotations: ```yaml # ✅ Correct - different prefixes internal.company.io/hostname: internal.example.com external-dns.alpha.kubernetes.io/hostname: example.com # ❌ Wrong - same prefix external-dns.alpha.kubernetes.io/hostname: internal.example.com external-dns.alpha.kubernetes.io/hostname: example.com # Second one overwrites first ``` ### Validation error: "annotation-prefix must end with '/'" **Problem:** The annotation prefix doesn't end with a forward slash. **Solution:** Always end your custom prefix with `/`: ```bash # ✅ Correct --annotation-prefix=custom.io/ # ❌ Wrong --annotation-prefix=custom.io ``` ### Provider-specific annotations not working **Problem:** Cloudflare/AWS-specific annotations are not being applied. **Solution:** Provider-specific annotations must use the same prefix as the hostname: ```yaml # If using custom prefix custom.io/hostname: example.com custom.io/cloudflare-proxied: "true" custom.io/ttl: "60" ``` ## See Also - [Configuration Precedence](configuration-precedence.md) - Understanding how external-dns processes configuration - [FAQ](../faq.md) - Frequently asked questions - [AWS Provider](../tutorials/aws.md) - AWS Route53 configuration - [Cloudflare Provider](../tutorials/cloudflare.md) - Cloudflare configuration