Week 16: Service Mesh — Istio & Linkerd Fundamentals

Week 14's NetworkPolicies controlled whether Pods can talk to each other. This week is about everything that happens once they do: encrypting that traffic automatically, retrying a failed call without the application knowing, and shifting a slice of production traffic to a new version before committing to it fully. A service mesh moves all of that out of application code and into infrastructure that sits transparently between every service, using the sidecar proxies of Istio or Linkerd — two of the most widely adopted meshes in production Kubernetes clusters today.

Module 13 of 22 Week 16 of 26 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain the sidecar pattern and the data plane / control plane split
  • Enable automatic mTLS and configure retries, timeouts and circuit breaking
  • Shift a percentage of live traffic to a new version with a VirtualService

1. The Sidecar Pattern & Data/Control Plane

A service mesh works by injecting a small proxy container — Envoy, in both Istio and (in Linkerd's case, a lighter Rust proxy) — into every Pod, alongside your application container. Every request your app sends or receives is transparently intercepted by that sidecar first. Your application code makes a perfectly ordinary HTTP call to another service's normal address; it has no idea a proxy is involved at all. The sum of every sidecar across the cluster is the mesh's data plane — it's what actually handles traffic. A separate control plane (istiod for Istio) configures every sidecar's behavior centrally and pushes updates out to them.

terminal — enabling sidecar injection for a namespace
kubectl label namespace production istio-injection=enabled

# any Deployment applied to this namespace from now on
# automatically gets an Envoy sidecar added to its Pods
kubectl apply -f k8s/orders-deployment.yaml -n production
kubectl get pods -n production
# NAME                     READY   STATUS
# orders-7d9f6b8c4d-x2k9p  2/2     Running   <- 2 containers: app + istio-proxy

That 2/2 is the whole pattern in one line: nothing in orders-deployment.yaml mentions Istio at all, yet every Pod it creates now runs an Envoy proxy alongside the app container, transparently. This is the central appeal of a mesh over building the same capabilities into every service's code — mTLS, retries, and traffic routing become a platform concern configured once, not a library every team has to import and keep in sync.

A mesh is real operational cost, not a free upgrade

Every sidecar is another container consuming CPU and memory, another hop of latency, and another moving part to debug when something breaks. A service mesh earns its keep in a system with many services talking to each other in ways that need consistent mTLS, retries and observability — a five-service app rarely needs one yet, and it's worth being able to say so in an interview rather than reaching for a mesh by default.

2. mTLS, Retries, Timeouts & Circuit Breaking

With sidecars in place, Istio can enforce mutual TLS (mTLS) between every service in the mesh — both sides of a connection present and verify a certificate, not just the server — without a single line of application code changing. Certificates are issued and rotated automatically by the control plane.

peer-authentication.yaml — mesh-wide strict mTLS
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT   # reject any plaintext traffic between meshed Pods

On top of encryption, the mesh can add resilience patterns directly at the proxy layer — retries on transient failures, timeouts so a slow dependency doesn't stall its caller forever, and circuit breaking so one struggling service doesn't get hammered by callers retrying into it and making the outage worse:

destination-rule.yaml — retries, timeout & circuit breaking for a service
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: inventory-service
spec:
  host: inventory-service
  trafficPolicy:
    connectionPool:
      http:
        http1MaxPendingRequests: 20
        maxRequestsPerConnection: 10
    outlierDetection:            # circuit breaking
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s      # eject an unhealthy Pod for 30s
      maxEjectionPercent: 50
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: inventory-service
spec:
  hosts: [inventory-service]
  http:
    - timeout: 2s
      retries:
        attempts: 3
        perTryTimeout: 500ms
        retryOn: 5xx,connect-failure
      route:
        - destination: { host: inventory-service }

outlierDetection is the circuit breaker: after 5 consecutive 5xx responses, that specific Pod is ejected from the load-balancing pool for 30 seconds, which gives it room to recover (or restart) instead of continuing to receive traffic it's already failing to serve. The 2-second overall timeout combined with a 500ms perTryTimeout and 3 retries means a caller gets a definitive answer within 2 seconds no matter what — never an indefinite hang waiting on a dependency that's stopped responding.

Retries can turn a partial outage into a total one

Retrying a request to an already-overloaded service multiplies the load it's receiving instead of relieving it — three retries on every failing call can mean 4x the traffic hitting a service that was already struggling. Pair retries with circuit breaking (outlier detection) so a consistently failing Pod gets pulled out of rotation instead of retried into indefinitely.

3. Traffic Shifting for Canary Releases

Week 15's rolling updates replace old Pods with new ones gradually, but every request still eventually reaches the new version — there's no way to say "send only 10% of traffic there and watch it." A mesh's VirtualService can split traffic by weight across two versions of the same service, running side by side, which is what makes a genuine canary release possible.

canary-virtualservice.yaml — 90/10 traffic split by version
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: checkout-service
spec:
  hosts: [checkout-service]
  http:
    - route:
        - destination:
            host: checkout-service
            subset: v1
          weight: 90
        - destination:
            host: checkout-service
            subset: v2      # the canary
          weight: 10
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: checkout-service
spec:
  host: checkout-service
  subsets:
    - name: v1
      labels: { version: v1 }
    - name: v2
      labels: { version: v2 }

Both v1 and v2 run as ordinary Deployments distinguished only by a version label — the VirtualService is what decides how traffic splits between them, and that weight is just a number you can change: watch the canary's error rate and latency in your Week 13 dashboards at 10%, then move to 25%, 50%, and 100% only once you trust it, or set the weight back to 0 and delete the canary Deployment in seconds if something looks wrong — no rollback of a Deployment's rollout history required, because the old version never stopped running.

A canary is only as good as what you watch during it

Shifting 10% of traffic to a new version is worthless if nobody is comparing its error rate and latency against the stable version while it runs. Tag metrics and logs with the version label from the DestinationRule so your Grafana dashboard can show v1 and v2 side by side, and decide the next weight change from that comparison — not from "it's been running for a while and nothing paged."

4. Hands-on Exercise

Hands-on

Mesh two services and run a weighted canary between them

Install Istio on your local cluster and put the mesh's core features to work against two versions of the same app.

Requirements:

  1. Install Istio with istioctl install, label your namespace for sidecar injection, and confirm a redeployed Pod shows 2/2 containers running.
  2. Apply a mesh-wide STRICT PeerAuthentication and confirm with istioctl x describe pod that traffic between two meshed services is mTLS-encrypted.
  3. Deploy a service with a DestinationRule setting outlier detection, then deliberately make it return 5xx errors and confirm it gets ejected from the load-balancing pool.
  4. Deploy v1 and v2 of a small app behind a weighted VirtualService starting at 90/10, send a burst of requests with a loop, and confirm the observed split roughly matches the configured weights.
  5. Shift the weight to 100/0 in favor of v2 and confirm all new requests land on the new version with no Pod restarts required.
Hint

for i in $(seq 1 100); do curl -s checkout-service/version; done | sort | uniq -c from a debug Pod inside the mesh is a fast way to see the actual observed traffic split against your configured weights.

5. Knowledge Check

Three quick questions. Expand each to check your answer.

Q1

Why does the application container never need to know a sidecar proxy exists?

The mesh transparently intercepts network traffic at the Pod level using iptables rules set up when the sidecar is injected, so every outbound and inbound connection is routed through Envoy without the application changing its code or even its target address. That transparency is what lets a mesh add mTLS, retries and traffic shifting to existing services without touching a single line of their code.

Q2

Why is pairing retries with circuit breaking (outlier detection) important, rather than configuring retries alone?

Retries alone can amplify load onto a service that's already struggling — each failed call now generates multiple retried calls against the same overloaded target. Outlier detection ejects a consistently failing Pod from the pool for a cooldown period, so retries land on healthy Pods instead of continuing to hammer the one that's already failing.

Q3

What can a weighted VirtualService canary do that a Deployment's rolling update alone cannot?

A rolling update eventually routes all traffic to the new version regardless of how it's performing — there's no way to hold it at a fixed, small percentage of live traffic for observation. A weighted VirtualService keeps both versions running simultaneously and lets you set and adjust the exact traffic split, so a problem shows up against only 10% of real users instead of the entire fleet.