DevOpsil
Nginx
87%
Needs Review

Envoy Proxy: Architecture, xDS Configuration, and Getting Started

Muhammad HassanMuhammad Hassan5 min read

Why Envoy Exists

HAProxy and Nginx were designed for traditional load balancing. Envoy was designed for service-to-service communication in distributed systems. The difference is architectural:

  • Envoy is L3/L4/L7-aware — it understands HTTP/2, gRPC, and Thrift natively
  • Envoy has first-class observability — every request generates stats, traces, and logs by default
  • Envoy is dynamically configurable — its xDS API lets a control plane push config changes without restarts
  • Envoy runs as a sidecar — one Envoy per service instance, forming a service mesh

Understanding Envoy's building blocks makes both standalone use and service mesh deployments (Istio, AWS App Mesh) comprehensible.


Core Concepts

Envoy's configuration model has four key primitives:

PrimitiveDescription
ListenerBinds to an IP:port, receives incoming connections
Filter ChainPipeline of network/HTTP filters applied to traffic
RouteMaps incoming requests to clusters
ClusterUpstream service pool with load balancing and health checks

Traffic flows: Listener → Filter Chain → Route → Cluster → Upstream


Installation

# Add Envoy apt repo (Ubuntu/Debian)
sudo apt install -y debian-archive-keyring apt-transport-https lsb-release ca-certificates gnupg
curl -sL 'https://deb.dl.getenvoy.io/public/gpg.8115BA8E629CC074.key' | sudo gpg --dearmor -o /usr/share/keyrings/getenvoy-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/getenvoy-keyring.gpg] https://deb.dl.getenvoy.io/public/deb/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/getenvoy.list
sudo apt update && sudo apt install envoy -y

# Or run via Docker
docker run --rm envoyproxy/envoy:v1.32-latest --version

# Verify
envoy --version

Static Configuration

Envoy configuration is YAML (or JSON). A complete static config:

envoy.yaml:

static_resources:

  listeners:
    - name: listener_0
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 10000
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: service_backend

  clusters:
    - name: service_backend
      connect_timeout: 5s
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: service_backend
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 127.0.0.1
                      port_value: 3000

admin:
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901

Run Envoy:

envoy -c envoy.yaml
# Or with Docker:
docker run --rm -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml \
  -p 10000:10000 -p 9901:9901 \
  envoyproxy/envoy:v1.32-latest

HTTP Routing with Multiple Clusters

Route different paths to different backends:

route_config:
  virtual_hosts:
    - name: main
      domains: ["*"]
      routes:
        # Route /api/* to API cluster
        - match:
            prefix: "/api/"
          route:
            cluster: api_cluster
            timeout: 30s

        # Route /static/* to static asset cluster
        - match:
            prefix: "/static/"
          route:
            cluster: static_cluster

        # Default
        - match:
            prefix: "/"
          route:
            cluster: web_cluster

Load Balancing Policies

clusters:
  - name: web_cluster
    lb_policy: LEAST_REQUEST    # Route to server with fewest active requests
    load_assignment:
      cluster_name: web_cluster
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address: { address: 10.0.1.10, port_value: 8080 }
              load_balancing_weight: 1
            - endpoint:
                address:
                  socket_address: { address: 10.0.1.11, port_value: 8080 }
              load_balancing_weight: 2    # gets 2x traffic

Available policies: ROUND_ROBIN, LEAST_REQUEST, RING_HASH, RANDOM, MAGLEV


Health Checking

clusters:
  - name: web_cluster
    health_checks:
      - timeout: 3s
        interval: 10s
        unhealthy_threshold: 3      # fail 3x to mark unhealthy
        healthy_threshold: 2        # pass 2x to mark healthy
        http_health_check:
          path: /health
          expected_statuses:
            - start: 200
              end: 299

Observability: Stats and Tracing

Envoy exposes rich metrics at the admin endpoint:

# Admin dashboard
curl http://localhost:9901/

# All stats
curl http://localhost:9901/stats

# Filter stats
curl http://localhost:9901/stats?filter=cluster.web_cluster

# Prometheus format
curl http://localhost:9901/stats/prometheus

# Ready check
curl http://localhost:9901/ready

Enable Zipkin tracing:

static_resources:
  ...

tracing:
  http:
    name: envoy.tracers.zipkin
    typed_config:
      "@type": type.googleapis.com/envoy.config.trace.v3.ZipkinConfig
      collector_cluster: zipkin
      collector_endpoint: "/api/v2/spans"
      collector_endpoint_version: HTTP_JSON

Circuit Breaking

Prevent cascading failures by limiting pending requests and retries:

clusters:
  - name: web_cluster
    circuit_breakers:
      thresholds:
        - priority: DEFAULT
          max_connections: 100
          max_pending_requests: 100
          max_requests: 1000
          max_retries: 3

When thresholds are exceeded, Envoy returns 503 immediately rather than queuing indefinitely.


The xDS Dynamic API

In production (and in Istio), Envoy configuration is pushed dynamically via the xDS API rather than static YAML. A control plane sends:

  • LDS (Listener Discovery Service) — listener config
  • RDS (Route Discovery Service) — routing rules
  • CDS (Cluster Discovery Service) — cluster definitions
  • EDS (Endpoint Discovery Service) — individual endpoint IPs
dynamic_resources:
  lds_config:
    resource_api_version: V3
    api_config_source:
      api_type: GRPC
      transport_api_version: V3
      grpc_services:
        - envoy_grpc:
            cluster_name: xds_cluster
  cds_config:
    resource_api_version: V3
    api_config_source:
      api_type: GRPC
      transport_api_version: V3
      grpc_services:
        - envoy_grpc:
            cluster_name: xds_cluster

static_resources:
  clusters:
    - name: xds_cluster
      type: STATIC
      load_assignment:
        cluster_name: xds_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: control-plane.example.com
                      port_value: 18000

This is how Istio works: Istiod is the xDS control plane, every pod's Envoy sidecar receives routing rules dynamically.


Envoy as a Sidecar vs. Edge Proxy

RoleDescription
Edge proxyOne Envoy at the cluster ingress (like an Nginx/HAProxy replacement)
SidecarOne Envoy per pod/service, intercepting all in/outbound traffic
Service meshAll sidecars + a control plane = Istio, Consul Connect, AWS App Mesh

For standalone edge proxy use: use static config as shown above. For Kubernetes service mesh: use Istio or Consul (they manage Envoy configuration automatically).

Share:

Was this article helpful?

Muhammad Hassan
Muhammad Hassan

Network & Traffic Engineer

Packets don't lie. I design and troubleshoot the network layer that everything else depends on — Nginx, Envoy, HAProxy, DNS, CDNs, and everything in between. If it touches a socket, it's my problem.

Related Articles

More in Nginx

View all →

Discussion