Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
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 != "external-dns.alpha.kubernetes.io/" {
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
25 changes: 25 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,31 @@ In larger clusters with many resources which change frequently this can cause pe
If only some resources need to be managed by an instance of external-dns then label filtering can be used instead of ingress class filtering (or legacy annotation filtering).
This means that only those resources which match the selector specified in `--label-filter` will be passed to the controller.

**Split horizon DNS with custom annotation prefixes**

For more advanced split horizon scenarios, you can use the `--annotation-prefix` flag to configure different instances to read different sets of annotations from the same resources. This is useful when you want a single Service or Ingress to create records in multiple DNS zones (e.g., internal and external).

For example:

```bash
# Internal DNS instance
--annotation-prefix=internal.company.io/ --provider=aws --aws-zone-type=private

# External DNS instance
--annotation-prefix=external-dns.alpha.kubernetes.io/ --provider=aws --aws-zone-type=public
```

Then annotate your resources with both prefixes:

```yaml
metadata:
annotations:
internal.company.io/hostname: app.internal.company.com
external-dns.alpha.kubernetes.io/hostname: app.company.com
```

See the [Split Horizon DNS guide](advanced/split-horizon.md) for detailed examples and configuration.

## How do I specify that I want the DNS record to point to either the Node's public or private IP when it has both?

If your Nodes have both public and private IP addresses, you might want to write DNS records with one or the other.
Expand Down
1 change: 1 addition & 0 deletions docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
| `--skipper-routegroup-groupversion="zalando.org/v1"` | The resource version for skipper routegroup |
| `--[no-]always-publish-not-ready-addresses` | Always publish also not ready addresses for headless services (optional) |
| `--annotation-filter=""` | Filter resources queried for endpoints by annotation, using label selector semantics |
| `--annotation-prefix="external-dns.alpha.kubernetes.io/"` | Annotation prefix for external-dns annotations (default: external-dns.alpha.kubernetes.io/) |
| `--[no-]combine-fqdn-annotation` | Combine FQDN template and Annotations instead of overwriting (default: false) |
| `--compatibility=` | Process annotation semantics from legacy implementations (optional, options: mate, molecule, kops-dns-controller) |
| `--connector-source-server="localhost:8080"` | The server to connect for connector source, valid only when using connector source |
Expand Down
Loading
Loading