Envoy’s configuration isn’t static; it’s a dynamic, real-time control plane that services talk to, not through.

Let’s watch this in action. Imagine a simple Go service, frontend-go, that needs to call another service, backend-go.

// frontend-go/main.go
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

func main() {
	http.HandleFunc("/", handler)
	log.Println("Frontend service starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
	log.Println("Received request for /")
	resp, err := callBackend()
	if err != nil {
		log.Printf("Error calling backend: %v", err)
		http.Error(w, fmt.Sprintf("Failed to call backend: %v", err), http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "Frontend response: %s", resp)
}

func callBackend() (string, error) {
	client := &http.Client{
		Timeout: 5 * time.Second,
	}
	resp, err := client.Get("http://backend-go:8080/data") // This URL is the key
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return string(body), nil
}

And the backend-go service:

// backend-go/main.go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/data", handler)
	log.Println("Backend service starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
	log.Println("Received request for /data")
	fmt.Fprintf(w, "Hello from backend-go!")
}

Without Envoy, frontend-go would directly try to resolve backend-go via DNS and connect to its IP. But with Envoy as a sidecar, the frontend-go service always makes its outbound HTTP calls to localhost:9901 (or whatever port Envoy is listening on for outbound traffic). Envoy, configured by a control plane, then intercepts this request and routes it to the actual backend-go service.

Here’s the typical setup:

  1. Go Services: These are built as usual, unaware of the mesh. They listen on their standard ports (e.g., frontend-go on 8080, backend-go on 8080).
  2. Envoy Sidecar: A separate Envoy process runs alongside each Go service in the same Kubernetes pod or on the same host. This sidecar is configured to:
    • Listen for inbound traffic on the service’s port (e.g., frontend-go’s Envoy listens on 8080 for requests to frontend-go).
    • Listen for outbound traffic from the application container (e.g., frontend-go’s Envoy listens on 9901 for requests from frontend-go).
    • Proxy traffic to the actual application container or to other service sidecars.
  3. Control Plane (e.g., Istio, Consul Connect): This is the brain. It dynamically configures all the Envoy sidecars. It tells Envoy sidecars how to route traffic, what TLS certificates to use, how to implement circuit breakers, rate limiting, etc.

When frontend-go makes a request to http://backend-go:8080/data, it’s actually sending it to localhost:9901 (assuming the sidecar is configured for outbound traffic on 9901). The frontend-go Envoy sidecar intercepts this, consults its configuration from the control plane, and forwards the request to the backend-go Envoy sidecar. The backend-go Envoy sidecar then forwards it to the actual backend-go application listening on localhost:8080.

The magic is that the Go code doesn’t change. It still thinks it’s talking to backend-go:8080. The Envoy sidecar handles the network resolution, load balancing, retries, and security.

The crucial part of the Envoy configuration for outbound traffic often looks like this (simplified snippet from a control plane like Istio):

# Example snippet of Envoy's Cluster configuration, managed by a control plane
clusters:
- name: backend-go
  type: STRICT_DNS # Envoy will resolve backend-go
  connect_timeout: 0.25s
  lb_policy: ROUND_ROBIN
  outlier_detection:
    consecutive_5xx_errors: 1
    interval: 10s
    base_ejection_time: 30s
  load_assignment:
    cluster_name: backend-go
    endpoints:
    - lb_endpoints:
      - endpoint:
          address:
            socket_address:
              address: 10.0.1.5 # Actual IP of backend-go pod
              port_value: 8080 # The port backend-go is listening on
      - endpoint:
          address:
            socket_address:
              address: 10.0.1.6 # Another IP of backend-go pod
              port_value: 8080
  # ... other configurations like TLS, health checks, etc.

The control plane dynamically generates this. When frontend-go’s Envoy receives a request destined for backend-go, it looks up the backend-go cluster. If it’s using STRICT_DNS, it will resolve backend-go (which the Kubernetes DNS service resolves to the service’s ClusterIP and then to pod IPs). Envoy then load balances across the available endpoints for that cluster. If a request fails (e.g., returns a 5xx error), Envoy, configured with outlier_detection, can temporarily eject that endpoint from the load balancing pool, preventing further requests from hitting a failing instance.

The control plane then injects rules into the inbound Envoy configuration on the backend-go sidecar. This inbound configuration tells the backend-go Envoy to listen on 8080 and forward any valid requests to the actual backend-go application process (usually on a different, internal port like 8081 or 9901 depending on the setup).

The truly mind-bending part is that your Go application isn’t just making HTTP requests; it’s making network calls that are being intercepted and rewritten at the network layer by Envoy. Your application code uses a standard http.Client and a standard URL like http://backend-go:8080, but Envoy is the one performing the DNS resolution, selecting the upstream IP, managing the connection pool, applying retry logic, and potentially enforcing TLS between the sidecars. The Go application code itself remains blissfully unaware of this entire complex dance happening around it.

What often trips people up is assuming the Go service needs to be aware of Envoy. It doesn’t. The integration happens entirely at the network proxy level, managed by the control plane.

The next challenge is often implementing mutual TLS (mTLS) between services using the service mesh.

Want structured learning?

Take the full Golang course →