docs/proposal/004-gateway-api-annotation-placement.md
---
title: "Gateway API Annotation Placement Clarity"
version: v1alpha1
authors: "@lexfrei"
creation-date: 2025-10-23
status: provisional
---
The annotations documentation indicates that Gateway API sources support various annotations, but it does not clearly specify which Kubernetes resource (Gateway vs HTTPRoute/GRPCRoute/TLSRoute/etc.) these annotations should be placed on. This ambiguity leads to user confusion and misconfigurations.
This proposal aims to:
Users frequently misconfigure annotations when using Gateway API sources because the current documentation uses "Gateway" as the source name in the annotation support table, which is ambiguousit refers to gateway-api sources generically, not the Gateway resource specifically.
Based on the source code (source/gateway.go):
Gateway resource annotations:
external-dns.alpha.kubernetes.io/target - read from Gateway
(line ~380)Route resource annotations (HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRoute):
external-dns.alpha.kubernetes.io/hostname - read from Routeexternal-dns.alpha.kubernetes.io/ttl - read from Routeexternal-dns.alpha.kubernetes.io/controller - read from Routecloudflare-proxied, aws/*, scw/*, etc.) - read from Route
(line ~242)This separation aligns with Gateway API architecture:
However, users expect provider-specific annotations to work on Gateway (similar to how target works), leading to silent failures.
As a platform engineer, I set up a Gateway with the external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
annotation, expecting all DNS records for Routes using this Gateway to be proxied through Cloudflare. However, the
annotation is silently ignored, and records are created without proxy, leading to unexpected traffic routing and
security issues.
Root cause: Had to dive into source code to discover that provider-specific annotations are only read from Route resources, not Gateway resources.
Current workaround: Must copy the cloudflare-proxied annotation to every HTTPRoute manually.
As a user, I want to specify different target DNS records for specific hosts while sharing a common Gateway. I
added external-dns.alpha.kubernetes.io/target annotation on HTTPRoute to override the Gateway's target for one
specific host, but it doesn't work - the annotation is ignored on HTTPRoute.
Root cause: The target annotation must be on the Gateway resource, not on Route resources. There's no way to
override targets on a per-Route basis.
Outcome: User had to find alternative workarounds to exclude specific hosts or create separate Gateway resources.
| Annotation Type | Gateway Resource | Route Resources (HTTPRoute, GRPCRoute, etc.) |
|---|---|---|
target | Read from Gateway | L Ignored |
hostname | L Not used | Read from Route |
ttl | L Not used | Read from Route |
controller | L Not used | Read from Route |
Provider-specific (cloudflare-proxied, aws/*, scw/*) | L Not used | Read from Route |
// source/gateway.go line ~380
// Target annotation is read from Gateway
override := annotations.TargetsFromTargetAnnotation(gw.gateway.Annotations)
// source/gateway.go line ~242
// Provider-specific annotations are read from Route
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots)
Where annots is derived from the Route's metadata (meta.Annotations), not the Gateway.
Implementation Status: Documentation improvements proposed in PR #5918.
Note: If Solution 2 (Annotation Merging) is implemented, the documentation from PR #5918 will require updates to reflect the new inheritance behavior.
Changes to docs/annotations/annotations.md:
Expand footnote [^4] or add a new section "Gateway API Annotation Placement" with a detailed table:
### Gateway API Annotation Placement
When using Gateway API sources (gateway-httproute, gateway-grpcroute, etc.), annotations must be placed on specific resources:
| Annotation | Placement | Example Resource |
|------------|-----------|------------------|
| `target` | Gateway | `kind: Gateway` |
| `hostname` | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `ttl` | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `controller` | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `cloudflare-proxied` | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `aws-*` (all AWS annotations) | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `scw-*` (all Scaleway annotations) | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
**Rationale**: The Gateway resource defines infrastructure (IP addresses, listeners), while Routes define application-level DNS records. Therefore, DNS record properties (TTL, provider settings) are configured on Routes.
Changes to docs/sources/gateway-api.md:
Add a new section after "Hostnames":
## Annotations
### Annotation Placement
ExternalDNS reads different annotations from different Gateway API resources:
- **Gateway annotations**: Only `external-dns.alpha.kubernetes.io/target` is read from Gateway resources
- **Route annotations**: All other annotations (hostname, ttl, provider-specific) are read from Route resources
#### Example: Cloudflare Proxied Records
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
annotations:
# Correct: target annotation on Gateway
external-dns.alpha.kubernetes.io/target: "203.0.113.1"
spec:
gatewayClassName: cilium
listeners:
- name: https
hostname: "*.example.com"
protocol: HTTPS
port: 443
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-route
annotations:
# Correct: provider-specific annotations on HTTPRoute
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
parentRefs:
- name: my-gateway
namespace: default
hostnames:
- api.example.com
rules:
- backendRefs:
- name: api-service
port: 8080
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: aws-gateway
annotations:
# Correct: target annotation on Gateway
external-dns.alpha.kubernetes.io/target: "alb-123.us-east-1.elb.amazonaws.com"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: weighted-route
annotations:
# Correct: AWS-specific annotations on HTTPRoute
external-dns.alpha.kubernetes.io/aws-weight: "100"
external-dns.alpha.kubernetes.io/set-identifier: "backend-v1"
spec:
parentRefs:
- name: aws-gateway
hostnames:
- app.example.com
❌ Incorrect: Placing provider-specific annotations on Gateway
kind: Gateway
metadata:
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true" # ❌ Ignored
❌ Incorrect: Placing target annotation on HTTPRoute
kind: HTTPRoute
metadata:
annotations:
external-dns.alpha.kubernetes.io/target: "203.0.113.1" # ❌ Ignored
Implementation effort: Low Maintenance burden: Minimal (documentation only) User benefit: Immediate clarity, reduced misconfiguration
Reference Implementation: PR #5998
Implement annotation merging logic where:
target — enabling per-Route target overridesProposed implementation (pseudocode):
// source/gateway.go - proposed changes
func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
// ... existing code ...
for _, route := range routes {
// Merge Gateway and Route annotations
// Route annotations take precedence over Gateway annotations
gwAnnots := gw.gateway.Annotations
rtAnnots := route.meta.Annotations
mergedAnnots := mergeAnnotations(gwAnnots, rtAnnots)
// Use merged annotations for all annotation processing
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(mergedAnnots)
ttl := annotations.TTLFromAnnotations(mergedAnnots, resource)
// ... rest of endpoint creation ...
}
}
// Helper function
func mergeAnnotations(gateway, route map[string]string) map[string]string {
merged := make(map[string]string, len(gateway)+len(route))
// Copy Gateway annotations (defaults)
for k, v := range gateway {
merged[k] = v
}
// Route annotations override Gateway defaults
for k, v := range route {
merged[k] = v
}
return merged
}
Example use case enabled by this approach:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: intranet-gateway
annotations:
# Default target for internal services
external-dns.alpha.kubernetes.io/target: "172.16.6.6"
# Set default for all Routes using this Gateway
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
external-dns.alpha.kubernetes.io/ttl: "300"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: internal-api
# Inherits: target=172.16.6.6, cloudflare-proxied=true, ttl=300 from Gateway
spec:
parentRefs:
- name: intranet-gateway
hostnames:
- api.internal.example.com
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: public-api
annotations:
# Override: expose this route to the public internet
external-dns.alpha.kubernetes.io/target: "203.0.113.1"
# Inherits: cloudflare-proxied=true, ttl=300 from Gateway
spec:
parentRefs:
- name: intranet-gateway
hostnames:
- api.example.com
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: static-assets
annotations:
# Override: disable proxying for static content
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
# Inherits: target=172.16.6.6, ttl=300 from Gateway
spec:
parentRefs:
- name: intranet-gateway
hostnames:
- static.internal.example.com
This example demonstrates a common use case: an intranet Gateway where most services are internal
(172.16.6.6), but specific Routes can be exposed publicly (203.0.113.1) by overriding the
target annotation.
Benefits:
Risks:
Mitigation strategies:
Implementation effort: Medium Maintenance burden: Medium (code + tests + docs) User benefit: Significant reduction in configuration overhead
Description: Keep current behavior and documentation as-is.
Pros:
Cons:
Recommendation: L Not recommended - problem is well-documented and affects user productivity
Description: Refactor source code to read all annotations from Gateway, not Routes.
Pros:
Cons:
Recommendation: L Not recommended - violates Gateway API design principles
Description: Allow annotations on both Gateway and Route, but error/warn if duplicates exist without clear precedence.
Pros:
Cons:
Recommendation: � Possible but adds complexity without solving core UX issue
Description: Introduce a new CRD that defines DNS configuration separately from Gateway and Route resources.
Example:
apiVersion: externaldns.k8s.io/v1alpha1
kind: GatewayDNSConfig
metadata:
name: cloudflare-defaults
spec:
gatewayRef:
name: my-gateway
defaults:
ttl: 300
providerSpecific:
- name: cloudflare-proxied
value: "true"
---
apiVersion: externaldns.k8s.io/v1alpha1
kind: RouteDNSConfig
metadata:
name: api-route-dns
spec:
routeRef:
kind: HTTPRoute
name: api-route
overrides:
ttl: 60 # Override Gateway default
Pros:
Cons:
Recommendation: � Potentially valuable long-term, but scope is too large for this specific issue
Description: Defer this work until the broader annotation standardization effort is resolved.
Pros:
Cons:
Recommendation: � Partial - implement documentation improvements now (Solution 1), reconsider annotation merging after standardization is resolved
Phased approach:
Immediate (v0.15.0 or next minor): Implement Solution 1 (Documentation Improvements)
Near-term: Review and merge Solution 2 (Annotation Merging)
Future (post-PR #5080 resolution): Re-evaluate if additional changes are needed
This approach provides immediate relief while keeping options open for more comprehensive solutions in the future.