External-DNS Integration¶
This guide covers integrating the Cloudflare Tunnel Gateway Controller with external-dns for automatic DNS record management.
Overview¶
The controller sets status.addresses on the Gateway with the tunnel CNAME (TUNNEL_ID.cfargotunnel.com). When external-dns is configured with Gateway API source, it automatically creates DNS records for your HTTPRoute hostnames.
Prerequisites¶
- Cloudflare Tunnel Gateway Controller installed
- external-dns installed and configured for Cloudflare
- Cloudflare API token with DNS edit permissions
external-dns Configuration¶
Helm Values¶
# external-dns values.yaml
provider:
name: cloudflare
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-dns-token
key: api-token
sources:
- gateway-httproute
- gateway-grpcroute
extraArgs:
- --gateway-namespace=cloudflare-tunnel-system
- --gateway-label-filter=app.kubernetes.io/name=cloudflare-tunnel-gateway-controller
API Token Permissions¶
The external-dns API token needs:
| Scope | Permission | Access |
|---|---|---|
| Zone | DNS | Edit |
| Zone | Zone | Read |
Separate Tokens
Use separate API tokens for the tunnel controller (Tunnel Edit) and external-dns (DNS Edit) following the principle of least privilege.
HTTPRoute Annotations¶
Add annotations to HTTPRoute for DNS configuration:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
annotations:
# Enable Cloudflare proxy (orange cloud)
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
# Custom TTL (seconds)
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
parentRefs:
- name: cloudflare-tunnel
namespace: cloudflare-tunnel-system
hostnames:
- app.example.com
rules:
- backendRefs:
- name: my-service
port: 80
Common Annotations¶
| Annotation | Description | Example |
|---|---|---|
external-dns.alpha.kubernetes.io/cloudflare-proxied | Enable Cloudflare proxy | "true" |
external-dns.alpha.kubernetes.io/ttl | DNS record TTL | "300" |
external-dns.alpha.kubernetes.io/target | Override CNAME target | custom.example.com |
external-dns.alpha.kubernetes.io/hostname | Override hostname | override.example.com |
How It Works¶
sequenceDiagram
participant User
participant K8s as Kubernetes
participant Ctrl as Controller
participant ExtDNS as external-dns
participant CF as Cloudflare
User->>K8s: Create Gateway
K8s->>Ctrl: Watch event (GatewayReconciler)
Ctrl->>K8s: Set Gateway status.addresses to tunnel CNAME
User->>K8s: Create HTTPRoute
K8s->>Ctrl: Watch event (RouteReconciler)
Ctrl->>CF: Update tunnel config
Ctrl->>K8s: Update HTTPRoute status
K8s->>ExtDNS: Watch Gateway/HTTPRoute
ExtDNS->>CF: Create DNS CNAME record
Note over CF: app.example.com → TUNNEL_ID.cfargotunnel.com The two controller writes come from separate reconcilers. GatewayReconciler sets status.addresses when the Gateway is first accepted, while the route reconcilers update the Cloudflare tunnel config and route status when an HTTPRoute or GRPCRoute changes. Creating an HTTPRoute re-enqueues the Gateway only to refresh AttachedRoutes and listener conditions, not to set status.addresses for the first time.
- User creates a Gateway; the controller sets Gateway
status.addressesto the tunnel CNAME once the Gateway is accepted - User creates an HTTPRoute with hostnames
- Controller updates the tunnel configuration in Cloudflare and the HTTPRoute status
- external-dns watches the Gateway and HTTPRoute
- external-dns creates a CNAME record pointing to the tunnel
Verifying DNS Records¶
Check that external-dns created the record:
# Check external-dns logs
kubectl logs --selector app.kubernetes.io/name=external-dns
# Query DNS
dig app.example.com CNAME
# Expected output
# app.example.com. 300 IN CNAME abc123.cfargotunnel.com.
Troubleshooting¶
DNS Record Not Created¶
- Check external-dns logs for errors:
- Verify Gateway has address set:
kubectl get gateway cloudflare-tunnel --namespace cloudflare-tunnel-system \
--output jsonpath='{.status.addresses[*].value}'
- Verify HTTPRoute is accepted:
Proxy Status Not Applied¶
Ensure the annotation is on the HTTPRoute, not the Gateway:
# Correct - annotation on HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
Multiple Gateways¶
If you have multiple Gateways, use label filters:
Production Recommendations¶
-
Use Cloudflare Proxy - Always set
cloudflare-proxied: "true"for DDoS protection and caching -
TTL Settings - When using Cloudflare proxy, TTL is managed by Cloudflare (set to "Auto")
-
Separate Zones - Consider separate DNS zones for different environments (prod, staging)
-
Policy Mode - Use
--policy=syncin production for external-dns to clean up stale records
Example: Complete Setup¶
external-dns version
Use external-dns v0.21.0 or later. The controller stores Gateway, HTTPRoute, and GRPCRoute resources under the stable gateway.networking.k8s.io/v1 API. external-dns migrated its Gateway API sources to v1 in v0.21.0; older releases query v1alpha2 and find no routes. This matches the minimum version cited in the ListenerSet guide.
---
# external-dns deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: external-dns
spec:
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.21.0
args:
- --source=gateway-httproute
- --source=gateway-grpcroute
- --provider=cloudflare
- --cloudflare-proxied
- --policy=sync
- --gateway-namespace=cloudflare-tunnel-system
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-dns-token
key: api-token
---
# HTTPRoute with DNS annotations
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
spec:
parentRefs:
- name: cloudflare-tunnel
namespace: cloudflare-tunnel-system
hostnames:
- app.example.com
- www.example.com
rules:
- backendRefs:
- name: my-service
port: 80