Skip to content
Published on

The Complete Contour Guide - Managing Kubernetes Traffic with Envoy-Based Ingress and HTTPProxy

Authors

Introduction

To route external traffic into cluster-internal services in Kubernetes, you need an ingress controller. The most widely used is ingress-nginx, but as your operational scale grows and multi-tenancy requirements appear, its limits start to show. Annotations on a single Ingress resource balloon into dozens, and changing one line of config reloads the entire NGINX process, momentarily dropping connections.

I ran headlong into this problem while operating an environment where multiple teams shared a single cluster. When Team A pushed a faulty ingress annotation, the entire NGINX config validation failed, and the impact rippled into Team B and Team C services. Ownership disputes over the root path were also endless.

This is when I started evaluating Contour. Contour is a CNCF incubating project, an ingress controller that uses the Envoy proxy as its data plane. There are two key points. First, when configuration changes, it does not reload Envoy but reflects changes dynamically via the xDS API, enabling zero-downtime updates. Second, through its own HTTPProxy CRD, it can safely implement delegation-based multi-tenancy.

This article covers Contour's architecture, HTTPProxy's routing model, multi-tenancy through delegation, TLS and authentication, Gateway API support, and the pitfalls and fixes you encounter in real operations. We will also touch on the broader direction of the Kubernetes ingress ecosystem as of 2026.

The State of the Ingress Ecosystem in 2026

Let us first establish the big picture. Kubernetes networking APIs are currently in a transitional period.

  • The Ingress API is frozen. The Ingress resource in networking.k8s.io/v1 receives no new features. Because its expressiveness is limited, every controller extended functionality through annotations, and these annotations are not portable across controllers.
  • Gateway API is the successor standard. Driven by SIG-Network, the Gateway API provides a role-based resource model (GatewayClass, Gateway, HTTPRoute) and is designed to directly solve Ingress's expressiveness limits and multi-tenancy problems.
  • Contour supports all three. It supports Ingress, its own HTTPProxy CRD, and the Gateway API together. HTTPProxy contains advanced features that Contour solved before the Gateway API was standardized, and that experience fed into the Gateway API design as well.

In short, for new projects, evaluate the Gateway API first, but if you need Contour's rich HTTPProxy features (delegation, inclusion, fine-grained traffic policy) or are already running on HTTPProxy, HTTPProxy remains a powerful choice.

AspectIngressHTTPProxyGateway API
Standard statusCore API (frozen)Contour-only CRDOfficial successor standard
Multi-tenancyAnnotation-dependentDelegation-based (strong)Role-separation model
Traffic policyLimitedRichExpanding
New featuresNone addedContinuously addedActively evolving
Recommended scenarioLegacy compatibilityAdvanced multi-tenancyAdopting the new standard

Contour Architecture - Separating Control Plane and Data Plane

Contour's design philosophy is clear separation of responsibility. Contour itself is the control plane, and Envoy is the data plane.

                     +---------------------------+
   kubectl apply     |   Kubernetes API Server    |
   (HTTPProxy / ---> |  (HTTPProxy, Ingress, ...) |
    Ingress)         +-------------+-------------+
                                   |
                          watch (informer)
                                   |
                                   v
                        +----------------------+
                        |   Contour (control   |
                        |   plane / xDS server)|
                        +----------+-----------+
                                   |
                          xDS API (gRPC stream)
                          LDS / RDS / CDS / EDS
                                   |
                                   v
   external traffic -> +----------------------+ ----> +----------------+
   (internet)          |   Envoy (data plane) |       | in-cluster      |
                       |   listener / route / |       | Pod / Service  |
                       |   cluster / endpoint |       +----------------+
                       +----------------------+

Let us walk through the flow step by step.

  1. A user applies an HTTPProxy or Ingress resource with kubectl.
  2. Contour, watching the Kubernetes API server through informers, detects the change.
  3. Contour converts this resource into an internal object graph and compiles it into xDS configuration that Envoy understands.
  4. Contour pushes the configuration to Envoy over an xDS gRPC stream. Listeners (LDS), routes (RDS), clusters (CDS), and endpoints (EDS) are each updated dynamically.
  5. Envoy receives the configuration, applies it without interruption, and handles the actual traffic.

xDS is the key here. NGINX-based controllers rewrite nginx.conf and reload the process when configuration changes. Envoy, by contrast, receives configuration dynamically via the xDS API, so there is no process restart or reload. Even if a single endpoint is added or a route rule changes, existing connections are unaffected. The busier the environment, the more this difference matters.

It also helps to know the deployment topology. Contour is typically deployed in two forms.

  • Contour Deployment + Envoy DaemonSet: Running Envoy as a DaemonSet on every node, exposed via hostNetwork or NodePort.
  • Contour Deployment + Envoy Deployment: Running Envoy as a separate Deployment, exposed via a LoadBalancer service. This is common in cloud environments.

Installation - Getting Up and Running Quickly

The simplest method is applying the official manifest.

# Apply the official quickstart manifest
kubectl apply -f https://projectcontour.io/quickstart/contour.yaml

# Check deployment status
kubectl get pods -n projectcontour

# Check the external IP of the Envoy service
kubectl get svc envoy -n projectcontour

In production, installing with a Helm chart to manage values is cleaner.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm install my-contour bitnami/contour --namespace projectcontour --create-namespace

If you want to manage it yourself, you can also use the ContourDeployment CRD and Gateway provisioner that Contour provides. Here is an example of adjusting core component configuration via values.

# contour-values.yaml example (for conceptual illustration)
contour:
  replicaCount: 2
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
envoy:
  kind: daemonset
  service:
    type: LoadBalancer
  resources:
    requests:
      cpu: 250m
      memory: 256Mi

HTTPProxy Basics - The Simplest Routing

Now to the main topic. HTTPProxy is the CRD Contour defines, providing a rich routing model that goes beyond the limits of Ingress. Let us start with the simplest form.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: basic-app
  namespace: web
spec:
  virtualhost:
    fqdn: app.example.com
  routes:
    - conditions:
        - prefix: /
      services:
        - name: app-service
          port: 80

virtualhost.fqdn is the domain this proxy handles. Under routes, you define which service receives traffic matching the conditions. The example above sends every path (/) coming into app.example.com to port 80 of app-service.

Weight-based splitting across multiple services (canary) is also simple.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: canary-app
  namespace: web
spec:
  virtualhost:
    fqdn: app.example.com
  routes:
    - conditions:
        - prefix: /
      services:
        - name: app-v1
          port: 80
          weight: 90
        - name: app-v2
          port: 80
          weight: 10

The weight value controls the traffic ratio. The configuration above gradually feeds traffic to the new version at a 90-to-10 split. Once the new version proves stable, you adjust the weight progressively to complete the migration.

Conditions - Path and Header-Based Routing

HTTPProxy conditions support not only prefix paths but also header-based matching. This lets you express varied traffic branching on the same domain.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: header-routing
  namespace: web
spec:
  virtualhost:
    fqdn: app.example.com
  routes:
    # API path goes to the backend API service
    - conditions:
        - prefix: /api
      services:
        - name: api-service
          port: 8080
    # If a specific header is present, go to the beta service
    - conditions:
        - prefix: /
        - header:
            name: x-canary
            exact: "true"
      services:
        - name: beta-service
          port: 80
    # Otherwise, the default path
    - conditions:
        - prefix: /
      services:
        - name: web-service
          port: 80

Header conditions support exact (exact match), contains, present (presence check), notpresent, and more. In the example above, only requests with the x-canary header set to true go to the beta service. This kind of branching is useful for canary testing or exposing features to internal users.

At the route level, you can also specify path rewrites, header add/remove, timeouts, and retries.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: route-policies
  namespace: web
spec:
  virtualhost:
    fqdn: app.example.com
  routes:
    - conditions:
        - prefix: /legacy
      pathRewritePolicy:
        replacePrefix:
          - prefix: /legacy
            replacement: /v2
      requestHeadersPolicy:
        set:
          - name: x-forwarded-prefix
            value: /legacy
        remove:
          - x-internal-token
      timeoutPolicy:
        response: 30s
      retryPolicy:
        count: 3
        retryOn: 5xx
      services:
        - name: legacy-service
          port: 80

pathRewritePolicy rewrites /legacy to /v2, requestHeadersPolicy manipulates headers, and timeoutPolicy and retryPolicy specify resilience policies. Things you handled as blobs of annotations in Ingress are expressed cleanly as structured fields.

Inclusion and Delegation - The Heart of Multi-Tenancy

This is the biggest reason to choose Contour. HTTPProxy can have a root proxy include child proxies in other namespaces through a mechanism called inclusion.

The structure is this. A platform team manages a root HTTPProxy that owns the domain and TLS, and each application team manages a child HTTPProxy in its own namespace that defines only routes. The root includes children under specific path conditions.

   Root HTTPProxy (namespace: ingress-root)
   fqdn: example.com, owns the TLS certificate
        |
        |  include (condition-based delegation)
        +-----------------------------+
        |                             |
        v                             v
   Child HTTPProxy               Child HTTPProxy
   (namespace: team-a)           (namespace: team-b)
   conditions: /team-a           conditions: /team-b
   defines routes only           defines routes only

The root proxy looks like this.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: example-root
  namespace: ingress-root
spec:
  virtualhost:
    fqdn: example.com
    tls:
      secretName: example-com-tls
  includes:
    - name: team-a-proxy
      namespace: team-a
      conditions:
        - prefix: /team-a
    - name: team-b-proxy
      namespace: team-b
      conditions:
        - prefix: /team-b

The child proxy in the team-a namespace defines only routes, with no fqdn.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: team-a-proxy
  namespace: team-a
spec:
  routes:
    - conditions:
        - prefix: /
      services:
        - name: team-a-service
          port: 80

The advantages of this structure are significant.

  • Domain ownership separation: The domain and TLS are managed solely by the platform team, so root path disputes disappear.
  • Permission isolation: Application teams only need permission to create HTTPProxy resources in their own namespace. RBAC controls this cleanly.
  • Fault isolation: One team's faulty configuration only invalidates that child proxy; it does not spread to other teams or the whole root. Contour rejects the bad child and keeps the rest working.

To further control delegation, you can use TLSCertificateDelegation to explicitly allow another namespace to reference a specific certificate secret.

apiVersion: projectcontour.io/v1
kind: TLSCertificateDelegation
metadata:
  name: example-delegation
  namespace: ingress-root
spec:
  delegations:
    - secretName: example-com-tls
      targetNamespaces:
        - team-a
        - team-b

TLS Configuration - Integrating with cert-manager

TLS comes down to referencing a secret under virtualhost.tls. Certificate issuance is typically left to cert-manager.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-tls
  namespace: ingress-root
spec:
  secretName: example-com-tls
  dnsNames:
    - example.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer

The secret issued this way is referenced in the HTTPProxy's tls.secretName. You can also configure HTTP-to-HTTPS redirect, minimum TLS version, and passthrough mode.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: tls-app
  namespace: web
spec:
  virtualhost:
    fqdn: secure.example.com
    tls:
      secretName: example-com-tls
      minimumProtocolVersion: "1.2"
  routes:
    - conditions:
        - prefix: /
      services:
        - name: secure-service
          port: 443

TLS passthrough, which forwards TCP traffic as-is, is configured by combining tcpproxy with tls.passthrough. It is used when the backend performs its own TLS termination.

Rate Limiting and External Authorization

Contour exposes Envoy's powerful features directly. The representative ones are rate limiting and external authorization.

Local rate limiting is handled within Envoy itself, without a separate service.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: ratelimited-app
  namespace: web
spec:
  virtualhost:
    fqdn: api.example.com
    rateLimitPolicy:
      local:
        requests: 100
        unit: minute
        burst: 20
  routes:
    - conditions:
        - prefix: /
      services:
        - name: api-service
          port: 80

The configuration above allows 100 requests per minute with a burst of 20. When the limit is exceeded, Envoy returns 429. Global rate limiting can integrate with an external RLS (Rate Limit Service) to apply a consistent limit across the entire cluster.

External authorization integrates with an ExtensionService to send every request to an authorization server first.

apiVersion: projectcontour.io/v1alpha1
kind: ExtensionService
metadata:
  name: authservice
  namespace: auth
spec:
  protocol: h2
  services:
    - name: auth-grpc
      port: 9443
---
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: authenticated-app
  namespace: web
spec:
  virtualhost:
    fqdn: secure.example.com
    tls:
      secretName: example-com-tls
    authorization:
      extensionRef:
        name: authservice
        namespace: auth
  routes:
    - conditions:
        - prefix: /
      services:
        - name: app-service
          port: 80

With this, every request coming into secure.example.com passes through the external authorization service in the auth namespace. You can delegate OIDC, JWT validation, and similar to the authorization server, offloading authentication logic from application code.

Health Checks and Load Balancing Strategy

At the service level, you can specify active health checks and load balancing algorithms.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: balanced-app
  namespace: web
spec:
  virtualhost:
    fqdn: app.example.com
  routes:
    - conditions:
        - prefix: /
      loadBalancerPolicy:
        strategy: WeightedLeastRequest
      services:
        - name: app-service
          port: 80
          healthCheckPolicy:
            path: /healthz
            intervalSeconds: 5
            timeoutSeconds: 2
            unhealthyThresholdCount: 3
            healthyThresholdCount: 2

loadBalancerPolicy.strategy supports RoundRobin, WeightedLeastRequest, Random, Cookie (session affinity), and more. healthCheckPolicy automatically removes unhealthy endpoints from the pool. Combined with Envoy's outlier detection, resilience rises significantly.

Moving to Gateway API - Contour's Support

As mentioned, the Gateway API is the successor standard to Ingress. As a Gateway API implementation, Contour supports GatewayClass, Gateway, and HTTPRoute. Expressing the same routing with the Gateway API looks like this.

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: contour
spec:
  controllerName: projectcontour.io/gateway-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: contour
  namespace: projectcontour
spec:
  gatewayClassName: contour
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
  namespace: web
spec:
  parentRefs:
    - name: contour
      namespace: projectcontour
  hostnames:
    - app.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-service
          port: 80

Here, the inclusion/delegation concept of HTTPProxy corresponds in the Gateway API to the Gateway's allowedRoutes and the namespace separation of HTTPRoute. The role-based model (infrastructure provider owns GatewayClass, cluster operator owns Gateway, app developer owns HTTPRoute) is cleanly separated.

The selection criteria are as follows.

  • If this is a new adoption and standard compatibility matters, prioritize the Gateway API.
  • If you need HTTPProxy-only advanced features (fine-grained inclusion, specific traffic policies) or already have a lot of HTTPProxy assets, keep HTTPProxy.
  • Using both APIs simultaneously is possible, but you must set operational rules so the two APIs do not handle the same domain at once.

Operations - Observability and Status Checks

The first thing you check in operations is the status of HTTPProxy. Contour records the validity of each resource in its status.

# Check HTTPProxy status - valid / invalid is shown in STATUS
kubectl get httpproxy -A

# Detailed status and error message for a specific proxy
kubectl describe httpproxy basic-app -n web

If the status is invalid, the cause is written in the description field. Common causes include orphaned (a child not included by a root), duplicate fqdn, and references to non-existent services.

To look directly at Envoy's actual configuration, use the Contour CLI or the Envoy admin interface.

# Dump the routes/clusters/endpoints Contour pushed to Envoy
kubectl exec -n projectcontour deploy/contour -- contour cli endpoints
kubectl exec -n projectcontour deploy/contour -- contour cli clusters

# Envoy statistics (after port-forwarding)
kubectl port-forward -n projectcontour ds/envoy 9001:9001
curl http://localhost:9001/stats | grep upstream_rq

Metrics are collected with Prometheus. Both Contour and Envoy expose metrics in Prometheus format, so you monitor request rate, latency, error rate (4xx/5xx), and upstream health from a Grafana dashboard. For access logs, it is recommended to configure JSON format to make parsing easy in your log pipeline.

Performance Tuning Points

As scale grows, check the following items.

ItemRecommendation
Envoy concurrencySet concurrency (worker threads) to match node CPU
Connection poolAdjust upstream maxConnections, maxRequests to protect backends
TimeoutsClearly separate global default timeouts from per-route timeouts
Resource requests/limitsEnvoy memory/CPU grows with traffic, so leave headroom
xDS update frequencyWith very many resources, monitor Contour's processing latency
Access logsAt high traffic, consider JSON logging cost and disk I/O

Envoy's concurrency value directly affects data-plane performance. The default matches the core count, but in environments where it occupies whole nodes as a DaemonSet, explicit tuning is more predictable. Connection pool limits are a safeguard preventing a slow backend from dragging everything down (head-of-line blocking).

Common Pitfalls and Troubleshooting

Here are the problems you frequently encounter in operations.

1. HTTPProxy status is invalid but traffic does not arrive

The most common. Check the description with kubectl describe. If a child proxy is not included by a root, it enters an orphaned state and is not routed. Verify that the name/namespace in the root's includes is correct.

2. Multiple root proxies declare the same fqdn

If two root HTTPProxy resources declare the same domain, Contour treats it as a conflict and invalidates one. fqdn must be unique cluster-wide. For multi-tenancy, do not split the fqdn; split paths with inclusion.

3. 503 upstream connect error

Mostly a service/endpoint problem. Check that the target Service's selector matches actual pods and that Endpoints are not empty. Checking the endpoints Envoy received with contour cli endpoints is fast.

4. TLS certificate not applied

Verify that the secret the secretName points to is in the same namespace, or if it is in a different namespace, that it is delegated via TLSCertificateDelegation. If cert-manager is issuing, check the Ready status of the Certificate resource.

5. Path rewrite behaves differently than intended

replacePrefix only substitutes the matched prefix portion. If the prefix condition and the replacePrefix prefix do not match, you get unexpected results. Check the condition's prefix and the rewrite rule together.

6. Configuration reflection delay

This happens when Contour takes time to process resources, or the xDS push to Envoy is slow. If you have thousands of resources, check Contour's logs and processing-latency metrics, and increase Contour replicas and resources if necessary.

Diagnostic commands collected in one place look like this.

# All HTTPProxy status at a glance
kubectl get httpproxy -A -o wide

# Trace errors in Contour logs
kubectl logs -n projectcontour deploy/contour --tail=200 | grep -i error

# Check clusters/endpoints Envoy received
kubectl exec -n projectcontour deploy/contour -- contour cli clusters
kubectl exec -n projectcontour deploy/contour -- contour cli endpoints

# Check upstream health via Envoy admin statistics
kubectl port-forward -n projectcontour ds/envoy 9001:9001
curl -s http://localhost:9001/clusters | grep health_flags

Conclusion

Contour goes beyond a simple ingress controller, combining Envoy's dynamic configuration capability with HTTPProxy's delegation model to shine in multi-tenant environments. To summarize the essentials again:

  • xDS-based zero-downtime configuration application enables stable updates even in high-traffic environments.
  • Inclusion and delegation separate domain ownership, isolate faults, and control permissions with RBAC, implementing safe multi-tenancy.
  • Rich traffic policies (rate limiting, external authorization, health checks, retries) are handled as structured fields without blobs of annotations.
  • Gateway API support secures a migration path to the future standard.

As of 2026, the Ingress API is frozen and the Gateway API is settling in as the successor standard. For new projects, evaluate the Gateway API first, but Contour's HTTPProxy still retains its value as a powerful multi-tenancy tool. A major advantage of this project is that you can travel both paths with Contour.

I encourage you to stand up Contour on a cluster yourself, create one small child HTTPProxy, and see with your own eyes how delegation works. You will escape annotation hell and feel the comfort of structured traffic management.

References