In 05-04 we left the exact-weight canary between internal services open: the Kubernetes Service spreads traffic per connection and by number of replicas, and to send exactly 10% of the gateway's calls to orders-service v2 you need something that understands HTTP between pods. That "something" is the same piece that would solve other problems every TechCorp service is repeating in its code: timeouts, retries, encryption between services, traffic metrics. A service mesh takes all of that out of the applications and puts it into the infrastructure, with a proxy next to each pod and a control plane that configures them. This lesson explains how it works (with Istio as the reference and Linkerd as the alternative), shows the YAML resources that close the canary of 05-04 and declare timeouts and retries for Orders→Catalog, lists honestly what it costs, and ends with TechCorp's decision: whether to adopt it now, and what to do if not. What the mesh touches in security (mTLS, 07-02), resilience in code (06-03) and traces (06-02) is only named here as a capability.
Contents
- The cross-cutting problems repeated in every service
- What a service mesh is: data plane and control plane
- Installing Istio in TechCorp's cluster and what changes in the pods
- Weighted routing:
VirtualServiceandDestinationRule(the 90/10 Orders canary) - Declarative timeouts and retries: Orders → Catalog
- Circuit breaking with
outlierDetection - Security as a capability:
PeerAuthenticationandAuthorizationPolicy - The observability the mesh gives for free (and the one it doesn't)
- Linkerd as a lighter alternative
- What a mesh costs
- Does TechCorp need a mesh? The decision
- What to do in the meantime
- The cross-cutting problems repeated in every service
Review what we have been asking of every service throughout the course:
| Concern | Where we solve it today | Repeated in |
|---|---|---|
| Timeout when calling another service | HTTP_TIMEOUT_MS in each service's HTTP client (04-03, 04-04) |
The six services and the gateway |
| Retries on transient failures | Code in catalogClient.js (and its full logic in 06-03) |
Every HTTP client |
| Circuit breaker | Code (06-03) | Every HTTP client |
| Encryption and authentication between services | Nothing yet; TLS only at the Ingress (07-02) |
All |
| Traffic metrics (requests, errors, latency) per service and route | @techcorp/common-http middleware exporting to Prometheus (06-01) |
All |
| Routing by weight / by header | NGINX Ingress or the Express gateway (05-04), only at the edge | Only the edge |
| Per-request (layer 7) load balancing | No: kube-proxy balances per connection (03-05) | — |
All of this has three characteristics: it is identical for every service, it is not business logic, and it depends on the language (the @techcorp/common-http library only works for Node; if the second service in Go from 04-01 arrives, it will have to be rewritten). The idea of the mesh: if they are network problems, let the network solve them.
- What a service mesh is: data plane and control plane
A service mesh has two parts:
- Data plane: a lightweight proxy deployed next to every pod (sidecar: Envoy in Istio,
linkerd2-proxyin Linkerd) that intercepts all the inbound and outbound traffic of the application container. The application believes it callshttp://catalog-service:3001; in reality it talks to its sidecar onlocalhost, which applies rules (timeout, retry, mTLS, metrics) and forwards to the destination's sidecar. Istio additionally offers the ambient mode (no sidecar: one proxy per node,ztunnel, for layer 4 and optional waypoints for layer 7). - Control plane: a central component (
istiodin Istio; Linkerd's control plane) that reads the Kubernetes resources and the mesh's own (VirtualService,DestinationRule...), and pushes the configuration to all the proxies, besides issuing the certificates for mTLS.
flowchart TB
subgraph CP["Control plane (namespace istio-system)"]
ISTIOD[istiod<br/>config + certificates]
end
subgraph NS["namespace techcorp (istio-injection=enabled)"]
subgraph PG["pod gateway"]
G[gateway :8080] --- GE[envoy sidecar]
end
subgraph PP1["pod orders-service v1"]
P1[orders-service :3002] --- PE1[envoy]
end
subgraph PP2["pod orders-service v2"]
P2[orders-service :3002] --- PE2[envoy]
end
subgraph PC["pod catalog-service"]
C[catalog-service :3001] --- CE[envoy]
end
end
ISTIOD -.->|xDS: rules| GE
ISTIOD -.-> PE1
ISTIOD -.-> PE2
ISTIOD -.-> CE
GE -->|90%, mTLS| PE1
GE -->|10%, mTLS| PE2
PE1 -->|timeout 2s, retry GET, mTLS| CE
The important thing about the picture: no line touches the services' code. Orders keeps doing fetch(CATALOG_URL + '/v1/products?ids=...') with its HTTP_TIMEOUT_MS; the sidecar adds the rest.
- Installing Istio in TechCorp's cluster and what changes in the pods
Conceptually (details vary by version), three steps:
istioctl install --set profile=default -y # installs istiod and Istio's ingress gateway in istio-system
kubectl label namespace techcorp istio-injection=enabled # from now on, every NEW pod in the namespace gets a sidecar
kubectl rollout restart deploy -n techcorp # recreate the existing pods so it gets injected
kubectl get pods -n techcorp # orders-service-... 2/2 Running ← two containers: the app and istio-proxyWhat changes in each pod, without touching any manifest of 05-02: an istio-proxy container (Envoy) next to the application's, an init container (or Istio's CNI) that redirects all the pod's traffic through the proxy with iptables, and a few dozen MB more memory per pod. The readinessProbe keeps pointing at the application container (Istio rewrites it to go through the proxy, transparently). And with Kustomize/GitOps (05-02, 05-03), the namespace label is a change in namespace.yaml, reviewed and applied like everything else.
- Weighted routing:
VirtualService and DestinationRule (the 90/10 Orders canary)
VirtualService and DestinationRule (the 90/10 Orders canary)Back to 05-04: Deployment orders-service (label version: v1) and orders-service-canary (version: v2), both selected by the orders-service Service. With Istio, the number of replicas no longer matters for the split: the weight is declared.
# k8s/orders-service/base/destinationrule.yaml
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: { name: orders-service, namespace: techcorp }
spec:
host: orders-service # the Kubernetes Service (05-02)
subsets: # subsets of pods, by label
- name: v1
labels: { version: v1 }
- name: v2
labels: { version: v2 }
---
# k8s/orders-service/base/virtualservice.yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: { name: orders-service, namespace: techcorp }
spec:
hosts: [orders-service] # applies to whoever calls http://orders-service:3002 inside the mesh (the gateway)
http:
- match: # canary by header (05-04, 03-04): the team tries v2 in production
- headers: { x-canary: { exact: orders } }
route:
- destination: { host: orders-service, subset: v2 }
- route: # everyone else: EXACT per-request weighted split
- destination: { host: orders-service, subset: v1 }
weight: 90
- destination: { host: orders-service, subset: v2 }
weight: 10Advancing the canary means changing 90/10 to 50/50 and then 0/100 (three commits in platform, or whatever Argo Rollouts/Flagger do on their own with metrics, 05-04); withdrawing it is 100/0. With one v2 replica and three v1 ones, 10% is 10%, not "one out of four". This is what 05-04 could not provide without a mesh.
- Declarative timeouts and retries: Orders → Catalog
The GET /v1/products?ids= call from Orders (04-04) with timeout and retries in the mesh:
# k8s/catalog-service/base/virtualservice.yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: { name: catalog-service, namespace: techcorp }
spec:
hosts: [catalog-service]
http:
- match:
- method: { exact: GET } # ONLY reads: they are idempotent (03-01), retrying is safe
route:
- destination: { host: catalog-service }
timeout: 2s # TOTAL time of the request, retries included
retries:
attempts: 2 # up to 2 retries (3 attempts in total)
perTryTimeout: 800ms # each attempt at most 0.8 s
retryOn: 5xx,connect-failure,reset # when: server error, could not connect, connection reset
- route: # everything else (if Catalog had POST/PUT): no retries
- destination: { host: catalog-service }
timeout: 2sThree warnings the YAML does not state on its own:
- Retry only idempotent operations. A repeated
GETdoes no harm; aPOST /v1/ordersretried by the proxy would create two orders if the first one arrived and the response was lost, unless the server applies theIdempotency-Key(03-01), and even then the proxy knows nothing about it. That is why thematchby method, and that is whyretriesis never put in a genericVirtualServicefor Orders. - The mesh's timeout and the code's coexist.
HTTP_TIMEOUT_MS=2000(04-03) stays in Orders: the sidecar cuts off at 2 s and so does the application. They must be consistent (the application's somewhat greater than or equal to the mesh's, so that it is the mesh that retries within its window); if the app's were 1 s, it would cut off before the mesh retried. - The mesh does not replace the failure logic. What Orders does when Catalog does not respond after the retries (degrade with the last price from
productTranslator, reject the order, enqueue) is a business decision and lives in the code: 06-03.
- Circuit breaking with
outlierDetection
outlierDetectionThe DestinationRule can also temporarily eject failing pods from load balancing (outlier detection, a passive per-instance circuit breaker) and limit connections:
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: { name: catalog-service, namespace: techcorp }
spec:
host: catalog-service
trafficPolicy:
connectionPool:
http: { http1MaxPendingRequests: 100, maxRequestsPerConnection: 10 }
outlierDetection:
consecutive5xxErrors: 5 # five consecutive 5xx from the same pod...
interval: 10s # ...evaluated every 10 s...
baseEjectionTime: 30s # ...take it out of load balancing for 30 s (growing if it relapses)
maxEjectionPercent: 50 # never more than half of the pods are ejectedHere we only present it: it protects from the "sick pod" without touching code, but it does not decide what to answer the user nor open the circuit for an entire dependency; the application-level circuit breaker (closed/open/half-open states, fallback response) belongs to 06-03, and the two complement each other.
- Security as a capability:
PeerAuthentication and AuthorizationPolicy
PeerAuthentication and AuthorizationPolicyWith one line, all the traffic between sidecars in the namespace becomes mTLS (encrypted and with each service's identity, certificates issued and rotated by istiod):
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: { name: default, namespace: techcorp }
spec:
mtls: { mode: STRICT } # only mTLS traffic is accepted; PERMISSIVE would also allow plain text during the migrationAnd an AuthorizationPolicy can say "only the gateway may call orders-service" or "nobody but Orders and Inventory talks to Catalog", by service identity, not by IP. It is a huge capability for very little effort, and one of the typical reasons for adopting a mesh. How mTLS works, which certificates lie underneath and how to get it without a mesh is 07-02; here it is enough to know that the mesh gives it for free.
- The observability the mesh gives for free (and the one it doesn't)
Since all the traffic goes through the proxies, the mesh produces without instrumenting anything: RED metrics (requests, errors, duration) per source/destination service, route and response code, in Prometheus format; the call graph in Kiali (who talks to whom, with what error rate, in real time); and trace spans per hop. What it does not give for free: the correlation of those spans into one trace of the complete request. Envoy cannot know that the outbound Orders→Catalog call belongs to the inbound gateway→Orders request; for that the application has to propagate the trace headers (traceparent, or the x-b3-* ones) from the inbound request to the outbound ones. That is exactly OpenTelemetry's job in 06-02, with or without a mesh.
- Linkerd as a lighter alternative
| No mesh (TechCorp today) | Linkerd | Istio | |
|---|---|---|---|
| Data plane | — | linkerd2-proxy (Rust), very small (~10-20 MB per pod) |
Envoy (C++), heavier (~50-100 MB per pod), or ambient without a sidecar |
| mTLS | No (TLS at the edge) | On by default, automatic, no configuration | Yes, with PeerAuthentication |
| Routing by weight / header | Only at the edge | Yes (Gateway API HTTPRoute, TrafficSplit) |
Yes, very complete (VirtualService) |
| Retries / timeouts | Code | Yes (HTTPRoute, ServiceProfile) |
Yes, highly configurable |
| Circuit breaking | Code | Basic | outlierDetection, connectionPool |
| Authorization policies | NetworkPolicy (07-04) | Yes (its own Server, AuthorizationPolicy) |
Yes, very expressive |
| Built-in observability | Whatever each service instruments | Golden metrics + linkerd viz dashboard |
Metrics + Kiali + Jaeger integration |
| Egress to external services, multi-cluster, VMs | — | Multi-cluster; fewer options | Everything, with more resources and concepts |
| Complexity and learning curve | None | Low: few CRDs, "works on install" | High: many CRDs, many options, many ways to get it wrong |
| Resource consumption | None | Low | Medium-high |
Linkerd installs with linkerd install | kubectl apply -f - and the linkerd.io/inject: enabled annotation on the namespace, and for many small teams it is "all the mesh they need": mTLS and metrics by default, declarative retries and weights, without Istio's breadth.
- What a mesh costs
- Latency: every hop goes through two proxies; the cost is milliseconds (less with Linkerd), but it adds up in long chains.
- Resources: one sidecar per pod. With 6 services × 3 replicas plus gateway and jobs, about 25 proxies; with Envoy, between 1 and 2 GB of cluster memory in sidecars alone.
- Operational complexity: another control plane that can go down (if
istiodgoes down, the proxies keep their last configuration, but new pods don't start correctly), CRDs to learn, harder debugging ("is it the app or the sidecar failing?"), interaction with jobs (aJobwith a sidecar does not finish until the proxy is killed: it must be annotated or use ambient). - Upgrades: the mesh has its own release cycle, coupled to Kubernetes versions; upgrading it means restarting every pod to renew sidecars.
- False sense of security: "we already have retries" without having thought about idempotency; "we already have mTLS" without authorization policies.
- Does TechCorp need a mesh? The decision
The facts: six services plus the gateway, all in Node with the same library, a four-person Platform team that has just set up Kubernetes, CI/CD and GitOps and that still has observability (Module 6) and security (Module 7) ahead. The benefits TechCorp would reap today: the exact weight of the Orders and Payments canary, internal mTLS and free traffic metrics. The costs: complexity for a team already at its limit, and one more layer to debug when the saga fails at 3 a.m.
Decision by Marta and the Platform team: do not adopt a mesh in the first phase. It will be reassessed when any of these conditions is met: (a) more than 10 services or a second language (the library stops covering everyone); (b) a mandatory mTLS requirement between services (for example, an audit of the Payments area), at which point the mesh is the cheapest way to get it; (c) the manual per-replica canary of Orders and Payments becomes a real bottleneck. When it comes, the first option to evaluate will be Linkerd, for cost and learning curve, with Istio ambient as the second if its routing capabilities are needed.
- What to do in the meantime
| Need | Solution without a mesh at TechCorp | Lesson |
|---|---|---|
| Timeouts, idempotent retries, circuit breaker | HTTP client of @techcorp/common-http (HTTP_TIMEOUT_MS, retries only on GET, breaker) |
06-03 |
| RED metrics per service | The library's Prometheus middleware | 06-01 |
| Traces | OpenTelemetry in the library (header propagation: needed anyway with a mesh) | 06-02 |
| Routing by weight and header | NGINX Ingress canary-weight at the edge; per-replica canary between services |
05-04 |
| Encryption and authentication between services | TLS at the Ingress; JWT propagated and verified in every service; NetworkPolicy for "who talks to whom" |
07-01, 07-02, 07-04 |
| Load balancing | Kubernetes Service (layer 4) and the gateway (layer 7 at the edge) |
03-05 |
The advantage of having concentrated the cross-cutting concerns in @techcorp/common-http since 04-01 is that, if the mesh arrives one day, whatever the mesh takes over (retries, traffic metrics) is removed from the library without touching the services; and if it does not arrive, the library remains TechCorp's "in-process mesh".
Common Mistakes and Tips
- Retries in the mesh for everything (
retryOn: 5xxwithout amatchby method): duplicate orders. Idempotent only. - Adopting the mesh "because everybody uses it" with five services and two platform people. The problems it solves have to exist first.
PeerAuthentication STRICTall at once with services still without a sidecar (or with Prometheus scraping from outside the mesh): everything stops talking.PERMISSIVEfirst,STRICTwhen 100% have a proxy.- A
Jobwith a sidecar that never finishes: the app container ends, Envoy keeps going. Annotation to skip injection in jobs, or ambient. - Believing the mesh gives complete traces without touching the app: without propagating
traceparent, every hop is a loose trace. - Tip: if you adopt a mesh, start with one namespace and one capability (mTLS
PERMISSIVE+ metrics), and add routing only when you need it; and measure p99 latency before and after.
Exercises
Exercise 1. Write the VirtualService that would let the Orders team, with Istio, reproduce the strategy of the 03-04 exercise but for Orders: for one week, only internal requests with X-Canary: orders go to orders-service v2; the rest, to v1; and explain in two sentences which other resource it needs and why the Deployment and the Service must not be touched.
Exercise 2. A colleague proposes declaring retries: { attempts: 3, retryOn: 5xx } in the orders-service VirtualService "so the gateway doesn't see errors during peaks". Explain what would happen with POST /v1/orders and with GET /v1/orders/{id}, and how what they want can be achieved without risk.
Exercise 3. Marta asks you for a one-sentence criterion for each of the three reassessment conditions of section 11 (more than 10 services or a second language, mandatory mTLS, canary as a bottleneck), and to state for each whether the first candidate would be Linkerd or Istio and why.
Solutions
Solution 1.
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: { name: orders-service, namespace: techcorp }
spec:
hosts: [orders-service]
http:
- match: [ { headers: { x-canary: { exact: orders } } } ]
route: [ { destination: { host: orders-service, subset: v2 } } ]
- route: [ { destination: { host: orders-service, subset: v1 } } ] # 100% v1 for the restIt needs the DestinationRule of section 4 that defines the v1/v2 subsets by the version label. Neither the Deployment nor the Service is touched because both already exist as 05-04 left them (two Deployments with version: v1/v2, one Service selecting by app); the mesh routes on top of the Service, reading the pods' labels. Withdrawing the canary means deleting the first rule; no manifest of 05-02 changes.
Solution 2. POST /v1/orders is not idempotent from the proxy's point of view: if Orders creates the order and the response is lost (or the pod returns 500 after committing the transaction, or dies right after writing), the sidecar retries and the second attempt arrives with the same Idempotency-Key; if the service implemented the key properly (03-01, 04-04), it answers the same result and there is no duplicate, but you are relying on all the service's POSTs being idempotent; and during a load peak, three attempts per request triple the load that causes the peak (retry storm). GET /v1/orders/{id} can indeed be retried. Solution: match by method: GET for the route with retries, retryOn: connect-failure,reset (not 5xx) at most for the rest, a short perTryTimeout, and a clear timeout at the gateway; the peak is handled with replicas (05-02, HPA in 06-04) and with rate limiting at the gateway (03-04), not with retries.
Solution 3. (a) More than 10 services or a second language: "when the library can no longer guarantee the same behavior in every service, the behavior moves to the network"; first candidate Linkerd (covers timeouts, retries, mTLS and metrics at low cost). (b) Mandatory mTLS: "when an audit demands encryption and identity between services, the mesh is cheaper than managing certificates in six services"; Linkerd, because mTLS is automatic and on by default. (c) Canary as a bottleneck: "when the per-replica percentage or the Ingress is not enough for Orders and Payments and we want to automate it with Argo Rollouts/Flagger"; here Istio (or Linkerd with Gateway API) for the richness of routing, unless Linkerd covers what is needed, in which case the lightweight option stays.
Conclusion
A service mesh moves to the infrastructure what every service repeats in its code: a proxy next to each pod (Envoy with Istio, linkerd2-proxy with Linkerd, or the ambient mode without a sidecar) and a control plane (istiod) that configures it, enabled with istioctl install and the istio-injection=enabled label on techcorp. With DestinationRule (v1/v2 subsets by the version label) and VirtualService we have closed the canary of 05-04 with an exact 90/10 weight and by X-Canary header; we have declared timeout: 2s and retries only for the GETs of Orders→Catalog, with the warning not to retry POST /v1/orders; we have presented outlierDetection as passive circuit breaking, PeerAuthentication STRICT and AuthorizationPolicy as security capabilities (07-02), and metrics and Kiali as free observability that still needs the application to propagate the trace headers (06-02); we have compared Istio, Linkerd and having no mesh, and spelled out their costs. TechCorp's decision is not to adopt it in the first phase and to reassess Linkerd when exceeding ten services, with mandatory mTLS or when the manual canary gets in the way; until then, the @techcorp/common-http library, the gateway and the resilience code play that role.
This closes the deployment and orchestration module: every service is a Docker image built with the same template and brought up locally with Docker Compose (05-01), runs in Kubernetes with its Deployment, Service, ConfigMap, Secret, Job and Ingress managed with Kustomize (05-02), reaches staging and production through a GitHub Actions pipeline with pacts, tagged images and GitOps (05-03), changes version without downtime with rolling, blue-green or canary (05-04) and knows what a mesh would give it and why it doesn't have one yet (05-05). What we still don't have is visibility: once the system is in production, how will we know that an order's saga has got stuck, which service responds slowly, or whether the Orders canary is going well? Module 6 starts right there: structured logging with Loki and metrics with Prometheus and Grafana (06-01), distributed traces with OpenTelemetry and Jaeger (06-02), the error handling and resilience in code that this module has kept deferring (06-03), scalability and performance with the HPA (06-04) and, finally, SLOs, alerts and incident management (06-05). It starts with logs and metrics.
Microservices Course
Module 1: Introduction to Microservices
- Basic Concepts of Microservices
- Advantages and Disadvantages of Microservices
- Comparison with the Monolithic Architecture
- When to Adopt Microservices: Decision Criteria
- The Course Case Study: TechCorp's Online Store
Module 2: Microservice Design
- Microservice Design Principles
- Decomposing Monolithic Applications
- Defining Bounded Contexts
- Data Management: One Database per Service
- Distributed Consistency: Sagas, CQRS and Event Sourcing
Module 3: Communication between Microservices
- RESTful APIs
- Asynchronous Messaging
- Communication Protocols: gRPC, GraphQL
- API Gateway and Backend for Frontend
- Service Discovery and Load Balancing
- API Contracts and Versioning
Module 4: Implementing Microservices
- Choosing Technologies and Tools
- Building a Simple Microservice
- Configuration Management
- Hands-On Integration: Consuming APIs and Publishing Events
- Testing Microservices: Unit, Integration and Contract Tests
Module 5: Deployment and Orchestration
- Containers and Docker
- Orchestration with Kubernetes
- CI/CD for Microservices
- Deployment Strategies: Rolling, Blue-Green and Canary
- Service Mesh: Istio and Linkerd
Module 6: Monitoring and Maintenance
- Monitoring and Logging
- Distributed Tracing with OpenTelemetry
- Error Handling and Recovery
- Scalability and Performance
- SLOs, Alerts and Incident Management
Module 7: Security in Microservices
- Authentication and Authorization
- Communication Security
- Security Practices
- Container and Kubernetes Security
