---
title: "Gateway API Annotation Placement Clarity"
version: v1alpha1
authors: "@lexfrei"
creation-date: 2025-10-23
status: provisional
---
Gateway API Annotation Placement Clarity¶
Table of Contents¶
Summary¶
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:
- Short-term: Improve documentation to explicitly clarify annotation placement
- Long-term: Consider implementing annotation inheritance from Gateway to Routes
Motivation¶
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.
Current Implementation Behavior¶
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 Route- Provider-specific annotations (e.g.,
cloudflare-proxied,aws/*,scw/*, etc.) - read from Route
(line ~242)
This separation aligns with Gateway API architecture:
- Gateway = infrastructure layer (IP addresses, listeners, load balancers)
- Routes = application layer (DNS records, routing rules, hostnames)
However, users expect provider-specific annotations to work on Gateway (similar to how target works), leading to silent failures.
Goals¶
- Clarify annotation placement in documentation to prevent user confusion
- Provide practical examples for common providers (Cloudflare, AWS, Scaleway)
- Define a clear, documented contract for where each annotation type should be placed
- Reduce support burden from repeated misconfigurations
Non-Goals¶
- This proposal does not address the broader annotation standardization effort discussed in PR #5080
- Redesigning the Gateway API source implementation
- Changing behavior for non-Gateway sources (Ingress, Service, etc.)
- Making breaking changes to existing Gateway API functionality
Proposal¶
User Stories¶
Story 1: Platform Engineer with Cloudflare (#5901)¶
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.
Story 2: User Attempting Route-Specific Targets (#4056)¶
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.
Current Behavior¶
Annotation Placement Matrix¶
| 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 |
Code References¶
// 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.
Proposed Solutions¶
Solution 1: Documentation Improvements (Short-term - Quick Win)¶
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
Example: AWS Route53 with Routing Policies¶
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
Common Mistakes¶
❌ 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
Solution 2: Annotation Inheritance and Merging (Long-term - Feature Enhancement)¶
Reference Implementation: PR #5998
Implement annotation merging logic where:
- Gateway annotations serve as defaults for all Routes attached to that Gateway
- Route annotations override Gateway annotations for specific Routes
- All annotations are inheritable, including
target— enabling per-Route target overrides
Proposed 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:
- Reduces configuration duplication
- Enables centralized defaults at Gateway level
- Maintains flexibility with Route-level overrides
- Better matches user mental model (infrastructure defaults + application overrides)
- Solves User Story 2: Enables per-Route target overrides without creating separate Gateways
Risks:
- Backward compatibility concerns (may change behavior for existing users)
- Increased code complexity
- Potential for confusion about precedence rules
- Need for comprehensive testing across all Gateway API Route types
Mitigation strategies:
- Feature flag to opt-in to new behavior initially
- Clear documentation of precedence rules
- Extensive test coverage
- Migration guide for users
Implementation effort: Medium
Maintenance burden: Medium (code + tests + docs)
User benefit: Significant reduction in configuration overhead
Drawbacks¶
Documentation-Only Solution¶
- Does not address the underlying UX issue (annotation duplication)
- Requires users to manually propagate settings across Routes
- Still allows silent failures if users misplace annotations
Annotation Merging Solution¶
- Adds complexity to the codebase
- Requires careful consideration of precedence rules
- May introduce unexpected behavior changes for existing users
- Needs comprehensive testing for edge cases (multiple Gateways, cross-namespace, etc.)
- Potential performance impact from annotation merging on every reconciliation
Alternatives¶
Alternative 1: Do Nothing (Status Quo)¶
Description: Keep current behavior and documentation as-is.
Pros:
- No implementation effort required
- No risk of introducing new bugs
- No breaking changes
Cons:
- Users continue to experience confusion and misconfigurations
- Increased support burden on maintainers and community
- Poor user experience compared to other sources (Ingress supports annotations more intuitively)
Recommendation: L Not recommended - problem is well-documented and affects user productivity
Alternative 2: Move All Annotations to Gateway Only¶
Description: Refactor source code to read all annotations from Gateway, not Routes.
Pros:
- Simplified mental model (one place for all annotations)
- Centralized configuration
Cons:
- Breaks Gateway API architecture - Routes define application-layer DNS records, so DNS properties belong on Routes
- Cannot have different settings per Route (e.g., different TTLs for api.example.com vs static.example.com)
- Loses flexibility that Route-level annotations provide
- Requires breaking change to existing implementations
Recommendation: L Not recommended - violates Gateway API design principles
Alternative 3: Support Annotations on Both with Strict Validation¶
Description: Allow annotations on both Gateway and Route, but error/warn if duplicates exist without clear precedence.
Pros:
- Provides flexibility
- Catches configuration errors explicitly
Cons:
- Confusing for users (two valid places to configure)
- Requires complex validation logic
- Still doesn’t solve the “defaults + overrides” use case
- More complex to document and support
Recommendation: � Possible but adds complexity without solving core UX issue
Alternative 4: Create Dedicated GatewayDNSConfig CRD¶
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:
- Clean separation of concerns
- Clear precedence model
- No annotations needed (type-safe CRDs)
- Aligns with Kubernetes resource composition patterns
Cons:
- Significant implementation effort (new CRDs, controllers, validation, etc.)
- Adds complexity with additional resources to manage
- Requires migration from annotation-based approach
- Diverges from how other sources work (Ingress, Service use annotations)
- May conflict with future annotation standardization efforts
Recommendation: � Potentially valuable long-term, but scope is too large for this specific issue
Alternative 5: Wait for Annotation Standardization (PR #5080)¶
Description: Defer this work until the broader annotation standardization effort is resolved.
Pros:
- Avoids potentially redundant work
- May be addressed as part of larger effort
Cons:
- PR #5080 is not yet ready for review and timeline is uncertain
- Users continue to experience issues in the meantime
- Documentation improvements are still valuable regardless of standardization outcome
Recommendation: � Partial - implement documentation improvements now (Solution 1), reconsider
annotation merging after standardization is resolved
Recommendation¶
Phased approach:
-
Immediate (v0.15.0 or next minor): Implement Solution 1 (Documentation Improvements)
- Low risk, high user value
- Can be merged quickly
- Addresses immediate pain points
-
Near-term: Review and merge Solution 2 (Annotation Merging)
- Reference implementation available: PR #5998
- Includes comprehensive test coverage
- Backward compatible (no breaking changes for existing configurations)
- Solves User Story 2 (per-Route target overrides)
-
Future (post-PR #5080 resolution): Re-evaluate if additional changes are needed
- Assess compatibility with annotation standardization outcomes
- Gather user feedback on the annotation inheritance behavior
This approach provides immediate relief while keeping options open for more comprehensive solutions in the future.