Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions charts/external-dns/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### Added

- Add option to set `annotationPrefix` ([#5889](https://github.com/kubernetes-sigs/external-dns/pull/5889)) _@lexfrei_

## [v1.19.0] - 2025-09-08

### Added
Expand Down
1 change: 1 addition & 0 deletions charts/external-dns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains
|-----|------|---------|-------------|
| affinity | object | `{}` | Affinity settings for `Pod` [scheduling](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/). If an explicit label selector is not provided for pod affinity or pod anti-affinity one will be created from the pod selector labels. |
| annotationFilter | string | `nil` | Filter resources queried for endpoints by annotation selector. |
| annotationPrefix | string | `nil` | Annotation prefix for external-dns annotations (useful for split horizon DNS with multiple instances). |
| automountServiceAccountToken | bool | `true` | Set this to `false` to [opt out of API credential automounting](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting) for the `Pod`. |
| commonLabels | object | `{}` | Labels to add to all chart resources. |
| deploymentAnnotations | object | `{}` | Annotations to add to the `Deployment`. |
Expand Down
3 changes: 3 additions & 0 deletions charts/external-dns/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ spec:
{{- if .Values.annotationFilter }}
- --annotation-filter={{ .Values.annotationFilter }}
{{- end }}
{{- if .Values.annotationPrefix }}
- --annotation-prefix={{ .Values.annotationPrefix }}
{{- end }}
{{- range .Values.managedRecordTypes }}
- --managed-record-types={{ . }}
{{- end }}
Expand Down
38 changes: 38 additions & 0 deletions charts/external-dns/tests/deployment-flags_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,41 @@ tests:
asserts:
- failedTemplate:
errorMessage: "'txtPrefix' and 'txtSuffix' are mutually exclusive"

- it: should configure custom annotation prefix
set:
annotationPrefix: "custom.io/"
asserts:
- contains:
path: spec.template.spec.containers[?(@.name == "external-dns")].args
content: "--annotation-prefix=custom.io/"

- it: should not include annotation-prefix flag when not set
set:
annotationPrefix: null
asserts:
- notContains:
path: spec.template.spec.containers[?(@.name == "external-dns")].args
content: "--annotation-prefix"

- it: should configure annotation prefix with other flags
set:
annotationPrefix: "internal.company.io/"
provider:
name: cloudflare
sources:
- service
- ingress
asserts:
- contains:
path: spec.template.spec.containers[?(@.name == "external-dns")].args
content: "--annotation-prefix=internal.company.io/"
- contains:
path: spec.template.spec.containers[?(@.name == "external-dns")].args
content: "--provider=cloudflare"
- contains:
path: spec.template.spec.containers[?(@.name == "external-dns")].args
content: "--source=service"
- contains:
path: spec.template.spec.containers[?(@.name == "external-dns")].args
content: "--source=ingress"
2 changes: 1 addition & 1 deletion charts/external-dns/tests/json-schema_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ tests:
enabled: "abrakadabra"
asserts:
- failedTemplate:
errorPattern: "Invalid type. Expected: [boolean,null], given: string"
errorPattern: "got string, want null or boolean"

- it: should fail if provider is null
set:
Expand Down
7 changes: 7 additions & 0 deletions charts/external-dns/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
"null"
]
},
"annotationPrefix": {
"description": "Annotation prefix for external-dns annotations (useful for split horizon DNS with multiple instances).",
"type": [
"string",
"null"
]
},
"automountServiceAccountToken": {
"description": "Set this to `false` to [opt out of API credential automounting](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting) for the `Pod`.",
"type": "boolean"
Expand Down
3 changes: 3 additions & 0 deletions charts/external-dns/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ labelFilter: # @schema type: [string,null]; default: null
# -- Filter resources queried for endpoints by annotation selector.
annotationFilter: # @schema type: [string,null]; default: null

# -- Annotation prefix for external-dns annotations (useful for split horizon DNS with multiple instances).
annotationPrefix: # @schema type: [string,null]; default: null

# -- Record types to manage (default: A, AAAA, CNAME)
managedRecordTypes: [] # @schema type: [array, null]; item: string; uniqueItems: true

Expand Down
7 changes: 7 additions & 0 deletions controller/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import (
webhookapi "sigs.k8s.io/external-dns/provider/webhook/api"
"sigs.k8s.io/external-dns/registry"
"sigs.k8s.io/external-dns/source"
"sigs.k8s.io/external-dns/source/annotations"
"sigs.k8s.io/external-dns/source/wrappers"
)

Expand All @@ -82,6 +83,12 @@ func Execute() {
log.Fatalf("config validation failed: %v", err)
}

// Set annotation prefix (required since init() was removed)
annotations.SetAnnotationPrefix(cfg.AnnotationPrefix)
if cfg.AnnotationPrefix != annotations.DefaultAnnotationPrefix {
log.Infof("Using custom annotation prefix: %s", cfg.AnnotationPrefix)
}

configureLogger(cfg)

if cfg.DryRun {
Expand Down
274 changes: 274 additions & 0 deletions docs/advanced/split-horizon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# 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
Loading
Loading