The most surprising thing about service meshes is that they don’t actually do anything for your application code.

Imagine you have a bunch of microservices, each running in its own container. When Service A needs to talk to Service B, it usually does so by making a direct network call. This works fine when you only have a few services, but as your system grows, managing these inter-service communications becomes a nightmare. You’ve got to handle retries, load balancing, encryption, metrics, tracing, and more, all within each service’s code.

This is where the service mesh comes in. Instead of your application code handling all that network complexity, a service mesh injects a small proxy, called a "sidecar," alongside each of your application containers. So, when Service A wants to talk to Service B, it doesn’t talk directly. Instead, it talks to its own sidecar proxy. This sidecar proxy then handles all the complex networking tasks and forwards the request to Service B’s sidecar proxy, which then delivers it to the Service B application container.

Here’s a simplified example. Let’s say we’re using Istio, a popular service mesh.

We have two services: frontend and backend.

frontend Deployment (simplified):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: my-frontend-app:v1 # Your actual application image
        ports:
        - containerPort: 8080
      # Istio injects the sidecar container here automatically

backend Deployment (simplified):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend
        image: my-backend-app:v1 # Your actual application image
        ports:
        - containerPort: 8080
      # Istio injects the sidecar container here automatically

When the frontend application wants to call the backend service, it makes a request to http://backend:8080. However, this request never actually leaves the frontend pod to go directly to the backend pod. Instead, the frontend application’s network stack is intercepted by the frontend sidecar proxy (usually an Envoy proxy). This sidecar proxy understands that http://backend:8080 refers to the backend service, and it consults the service mesh’s control plane for information about how to reach backend.

The control plane (e.g., Istio’s istiod) has a complete map of all services, their network locations, and any traffic management rules you’ve defined. Based on this, the frontend sidecar proxy might decide to:

  1. Load Balance: If there are multiple backend pods running (as in our example with 3 replicas), the frontend sidecar will intelligently choose one of them to send the request to, distributing the load.
  2. Retry: If the chosen backend pod is temporarily unavailable or the request times out, the frontend sidecar can automatically retry the request, perhaps to a different backend pod, without the frontend application code needing to know.
  3. Encrypt: The sidecar can automatically encrypt the traffic between the frontend sidecar and the backend sidecar using mTLS (mutual Transport Layer Security), ensuring secure communication without any changes to your application code.
  4. Trace: The sidecar can inject tracing headers into the request, allowing you to track the request’s journey across multiple services for debugging and performance analysis.
  5. Apply Policies: You can define routing rules. For example, you could tell the service mesh to send 10% of frontend traffic to a new version of backend (e.g., backend:v2) for canary testing. The frontend sidecar would enforce this rule.

The key takeaway is that all this sophisticated network logic is handled by the sidecar proxies, not by your application code. This liberates your developers to focus solely on business logic.

The service mesh itself is composed of two main parts:

  • Data Plane: This is the network of sidecar proxies (like Envoy) running alongside each service instance. They intercept and handle all inter-service communication.
  • Control Plane: This is the "brain" of the service mesh. It configures and manages the sidecar proxies. It pushes routing rules, policies, and telemetry configurations down to the data plane. In Istio, this is primarily istiod.

To control traffic, you define Custom Resource Definitions (CRDs) in Kubernetes. For instance, to send 10% of traffic to a new version of the backend service (backend-v2), you’d create an Istio VirtualService and DestinationRule.

Example VirtualService for traffic splitting:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: backend
spec:
  hosts:
  - backend
  http:
  - route:
    - destination:
        host: backend
        subset: v1 # Refers to a subset defined in DestinationRule
      weight: 90
    - destination:
        host: backend
        subset: v2 # Refers to a subset defined in DestinationRule
      weight: 10

And a corresponding DestinationRule to define the subsets:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: backend
spec:
  host: backend
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

This configuration tells the VirtualService to route 90% of traffic destined for backend to pods labeled version: v1 and 10% to pods labeled version: v2. The backend pods would need to be deployed with these labels, e.g., kubectl label deployment backend version=v1 and deploy another deployment for version=v2.

The power here is that you can change this traffic routing dynamically, without redeploying your applications, by simply updating these Kubernetes CRDs. The sidecar proxies automatically pick up the new rules from the control plane.

What most people don’t realize is that the sidecar proxy doesn’t just handle outgoing traffic; it also intercepts incoming traffic to its pod. This means that every single network request entering and leaving a pod is managed by a sidecar. This ubiquitous interception is what allows the service mesh to enforce policies, gather telemetry, and manage traffic at a granular level for every service.

The next logical step after understanding sidecars and traffic control is exploring how service meshes handle observability, particularly distributed tracing and metrics collection.

Want structured learning?

Take the full Computer Networking course →