Kubernetes Ingress and CDN Integration: Routing Patterns for Cloud-Native Apps
Kubernetes has become the standard orchestration platform for containerized applications, but connecting a Kubernetes cluster to a CDN requires careful architectural decisions about DNS management, TLS termination, health checking, and origin routing. The Ingress controller sits at the boundary between the CDN edge network and your cluster's internal service mesh, making it the most critical integration point in the entire delivery chain. Getting this integration wrong leads to cache misses, broken health checks, certificate errors, and the kind of intermittent failures that are nearly impossible to debug in production. For background on CDN architecture fundamentals, see the CDN Architecture: Edge, Origin, and Everything Between guide.
How Does a CDN Connect to a Kubernetes Cluster?
The connection between a CDN and a Kubernetes cluster follows a layered routing model. The CDN edge network receives end-user requests and either serves cached content or forwards cache misses to an origin server. In a Kubernetes environment, that origin server is the Ingress controller — a specialized load balancer pod that routes HTTP traffic to backend services based on hostname and path rules defined in Ingress resource manifests.
The typical architecture uses a cloud provider load balancer (AWS NLB, GCP External LB, or Azure Load Balancer) as the bridge between the CDN and the cluster. The CDN's origin configuration points to this load balancer's static IP or DNS name, and the load balancer forwards traffic to the Ingress controller pods. This separation provides a stable origin address that survives pod restarts, node failures, and cluster upgrades.
Configuring the Ingress Controller as CDN Origin
The Ingress controller needs specific configuration to work correctly as a CDN origin. Standard configurations assume the Ingress controller is the public-facing endpoint, but when a CDN sits in front, the controller becomes a backend that must cooperate with edge caching, header forwarding, and health monitoring. Here is a production-ready NGINX Ingress configuration:
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cdn-origin-ingress
annotations:
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
nginx.ingress.kubernetes.io/compute-full-forwarded-for: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "30"
nginx.ingress.kubernetes.io/proxy-send-timeout: "30"
nginx.ingress.kubernetes.io/server-snippet: |
real_ip_header CF-Connecting-IP;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 2400:cb00::/32;
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Cache-Status $upstream_cache_status;
add_header Cache-Control "public, max-age=3600, s-maxage=86400";
spec:
ingressClassName: nginx
tls:
- hosts:
- origin.example.com
secretName: origin-tls-cert
rules:
- host: origin.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 3000
The use-forwarded-headers annotation ensures the Ingress controller trusts the X-Forwarded-For and X-Forwarded-Proto headers set by the CDN, preserving the real client IP and protocol information through the proxy chain. The real_ip_header directive extracts the actual client IP from Cloudflare's CF-Connecting-IP header, with the set_real_ip_from directives restricting this trust to known CDN IP ranges.
DNS Architecture: External-DNS and CDN Origins
DNS configuration for CDN-fronted Kubernetes services requires a two-record pattern. The public domain (example.com) CNAMEs to the CDN's edge network. A separate origin subdomain (origin.example.com) resolves to the Ingress controller's load balancer. External-dns automates the origin record management by watching Kubernetes resources for annotations. For a deep dive into DNS routing mechanics, see DNS Resolution in CDN: How Users Reach the Nearest Edge.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cdn-origin
annotations:
external-dns.alpha.kubernetes.io/hostname: origin.example.com
external-dns.alpha.kubernetes.io/ttl: "300"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec:
ingressClassName: nginx
rules:
- host: origin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 8080
The cloudflare-proxied: "false" annotation is critical when using Cloudflare as both CDN and DNS provider. Without it, external-dns creates a proxied (orange cloud) record that routes traffic through Cloudflare's edge network, creating a loop where the CDN's origin pull hits itself. The origin record must be a direct DNS record (grey cloud) that resolves to the load balancer IP.
TLS Architecture: Cert-Manager and Dual Certificate Layers
CDN integration introduces a dual-certificate TLS model. The CDN terminates the public-facing TLS connection at its edge PoPs using either an automatically provisioned certificate or one you upload. A second TLS connection protects the link between CDN edge nodes and the Kubernetes Ingress controller. Cert-manager automates the origin certificate lifecycle. For details on TLS configuration patterns, see CDN SSL/TLS: Encryption from Edge to Origin.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: origin-cert
namespace: ingress-system
spec:
secretName: origin-tls-cert
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- origin.example.com
duration: 2160h # 90 days
renewBefore: 720h # 30 days before expiry
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-account
solvers:
- dns01:
cloudflare:
email: [email protected]
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
The DNS-01 challenge solver is essential for origin certificates behind a CDN. HTTP-01 challenges require the ACME server to reach the origin directly on port 80, which fails when a CDN intercepts the request. DNS-01 validates ownership by creating a TXT record, bypassing the CDN's request interception entirely. This approach works reliably regardless of CDN configuration.
Health Check Integration: CDN Meets Kubernetes Probes
Health checking across the CDN-to-Kubernetes boundary requires coordination between CDN origin health checks and Kubernetes readiness probes. The CDN monitors origin availability by polling a health endpoint. Kubernetes uses liveness and readiness probes to manage pod lifecycle. These two systems must align to prevent false failovers and traffic black holes. For health check architecture patterns, see Health Checks and Failover in CDN: Ensuring 99.99% Uptime.
apiVersion: v1
kind: Service
metadata:
name: health-aggregator
spec:
selector:
app: health-check
ports:
- port: 8081
targetPort: 8081
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: health-aggregator
spec:
replicas: 2
selector:
matchLabels:
app: health-check
template:
spec:
containers:
- name: health
image: registry.example.com/health-checker:1.4
ports:
- containerPort: 8081
env:
- name: SERVICES
value: "api-service:8080,web-service:3000"
- name: THRESHOLD
value: "1" # At least 1 healthy backend
livenessProbe:
httpGet:
path: /livez
port: 8081
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8081
periodSeconds: 5
The health aggregator pattern creates a dedicated service that probes multiple backend services and returns a composite health status. The CDN health check hits this aggregator endpoint. If any required backend is unhealthy, the aggregator returns 503, triggering the CDN's failover to a secondary origin or a stale-while-revalidate response. This prevents the CDN from routing traffic to a partially healthy cluster where some services are down.
Cache-Control Headers from Kubernetes Services
Kubernetes services behind a CDN must emit correct Cache-Control headers to control edge caching behavior. Without explicit headers, CDN behavior depends on provider defaults — some cache aggressively, others pass everything through. The Ingress controller can inject headers at the routing layer, or individual services can set them in application code. The Ingress annotation approach ensures consistent caching regardless of what individual microservices emit:
# Per-path cache control via Ingress annotations
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
location ~* ^/api/ {
add_header Cache-Control "private, no-store";
}
location ~* ^/assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~* ^/pages/ {
add_header Cache-Control "public, s-maxage=3600, stale-while-revalidate=86400";
}
The s-maxage directive is the key CDN-specific header. Unlike max-age which controls browser cache duration, s-maxage sets the CDN edge cache TTL independently. Combined with stale-while-revalidate, this creates a pattern where the CDN serves cached content for up to an hour, then serves stale content for an additional 24 hours while fetching a fresh copy from the origin asynchronously. For a complete cache header reference, see Cache-Control Headers: The Definitive Deep Dive.
Rolling Deployments and CDN Cache Coordination
Kubernetes rolling deployments create a window where old and new pod versions serve traffic simultaneously. When a CDN caches responses during this window, it may store a mix of old and new content. The safest approach combines versioned asset URLs with cache purge automation:
# deployment-hook.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: cdn-purge-{{ .Release.Revision }}
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-weight: "5"
helm.sh/hook-delete-policy: hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: purge
image: curlimages/curl:8.5.0
command:
- sh
- -c
- |
curl -X POST \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"prefixes":["example.com/pages/","example.com/api/v2/"]}'
env:
- name: ZONE_ID
valueFrom:
secretKeyRef:
name: cloudflare-credentials
key: zone-id
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-credentials
key: api-token
This Helm post-upgrade hook triggers a targeted CDN cache purge after every deployment, clearing only the prefixes that the new release modifies. Targeted purges minimize the origin load spike that a full cache flush would cause, while ensuring users see the latest version within seconds of deployment completion.
Real Client IP Extraction Through the Proxy Chain
When a CDN sits in front of a Kubernetes cluster, the original client IP passes through three to four proxy layers: CDN edge, cloud load balancer, Ingress controller, and application pod. Each layer appends to the X-Forwarded-For header, creating a chain like client, cdn-edge, lb-ip. The Ingress controller must extract the correct IP from this chain for logging, rate limiting, and geo-routing. Most CDN providers also set a dedicated header (CF-Connecting-IP for Cloudflare, True-Client-IP for Akamai, X-Azure-ClientIP for Azure Front Door) that contains only the original client IP without the proxy chain complexity.
Multi-Cluster Origin with CDN Failover
Production CDN deployments often use multiple Kubernetes clusters as origin pools for high availability. The CDN performs origin-level health checking and fails over between clusters when one becomes unhealthy. This architecture provides resilience against cluster-level failures, regional outages, and Kubernetes upgrade disruptions. Each cluster runs its own Ingress controller with its own load balancer IP, and the CDN monitors both origins independently. Active-active configurations serve traffic from the geographically nearest healthy cluster, reducing latency and providing natural load distribution.
Monitoring CDN-to-Kubernetes Request Flow
Observability across the CDN-to-Kubernetes boundary requires correlating metrics from three distinct systems: CDN analytics (cache hit ratio, origin response time, edge errors), cloud load balancer metrics (connection counts, latency distribution, health check results), and Kubernetes metrics (pod CPU/memory, request latency, error rates). The correlation key is a request ID header injected at the CDN edge and propagated through every layer:
# Ingress annotation to propagate CDN request ID
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header X-Request-ID $http_cf_ray;
proxy_set_header X-Trace-ID $http_x_trace_id;
access_log /var/log/nginx/access.log combined
if=$log_condition;
Cloudflare's CF-Ray header, Fastly's X-Request-ID, or AWS CloudFront's X-Amz-Cf-Id provide CDN-level request tracing. Forwarding these headers to application pods connects CDN-level issues (high origin response time, specific edge PoP errors) with pod-level diagnostics (slow database queries, OOM kills, connection pool exhaustion) through a single identifier.
Practical Recommendations
Start with the NGINX Ingress Controller if your team is new to CDN-Kubernetes integration — its annotation model and ConfigMap overrides provide the most flexibility for CDN-specific header manipulation without custom controller builds. Use the two-record DNS pattern from the beginning, even if you currently use only one cluster. Configure cert-manager with DNS-01 challenges for origin certificates. Set explicit Cache-Control headers at the Ingress layer rather than relying on CDN defaults, and implement a post-deployment cache purge hook in your CI/CD pipeline. Finally, propagate CDN request ID headers to every pod for end-to-end traceability across the edge-to-origin request path.