09 - Capstone Project
Why this matters
The previous nine modules each drilled one mesh capability in isolation. Real
mesh work is making them hold together: a namespace where injection is on,
every service is meshed under STRICT mTLS, one service is mid-canary between
two versions, an AuthorizationPolicy enforces who may call whom, and a
resilience policy you can prove works under injected failure. This capstone
asks you to stand all of that up at once, with no full solution given — the
integration is the test. If you can build and verify this, you can operate a
service mesh, not just recite its objects.
The project
Build a meshed multi-service application on your local kind cluster and prove
every mesh property end to end. Design the app yourself (three small services
is plenty — e.g. an edge storefront calling an orders service, which calls
a catalog service). Use throwaway images you already know from this track
(hashicorp/http-echo, curlimages/curl) — the point is the mesh, not the
app code.
Acceptance checklist
Your project is complete when all of the following are true and you can demonstrate each with a command, not just assert it:
- Istio installed and a namespace meshed. A dedicated namespace
(e.g.
shop) is labelled for sidecar injection, and every application Pod in it comes up2/2and appears inistioctl proxy-statusasSYNCED. - Multi-service app, all meshed. At least three services, each with its own ServiceAccount (so they have distinct mTLS identities), deployed and serving traffic through the mesh.
- STRICT mTLS across the namespace. A
PeerAuthenticationenforces STRICT; you can show withistioctl authn tls-checkthat calls use mTLS, and demonstrate that a freshly-created non-meshed Pod is rejected when it tries to call a meshed service. - Weighted canary on one service. One service (e.g.
catalog) has two versions (v1/v2) with aDestinationRuledefining subsets and aVirtualServicesplitting traffic by weight; you can step the weight (e.g. 90/10 → 50/50 → 0/100) and show the observed split tracks the weights. - AuthorizationPolicy restricting calls. An
AuthorizationPolicykeyed on mTLS principals enforces a real access matrix — e.g.ordersmay callcatalog, but the edgestorefrontmay not callcatalogdirectly. You can show the allowed call returns200and the forbidden one returns403. - Fault-injection test proving a retry/timeout works. One route has a
timeout and/or a bounded retry policy, and you use
VirtualServicefault injection to prove it: inject a delay longer than the timeout and show the timeout fires at the expected time, and/or inject a retryable abort and show the retry policy drops the caller-visible error rate. The proof is the measured before/after, not the YAML. - Observability confirming it all. You can point to the Kiali graph (or Istio metrics in Prometheus) showing the meshed topology, the mTLS padlocks, the canary split as weighted edges, and the authz denial as a red edge.
Hints (not a solution)
- Order of operations matters. Mesh everything first (label + create Pods,
verify
2/2) before turning on STRICT mTLS — flipping STRICT with an unmeshed Pod still around is the module-04 outage you already met. AddAuthorizationPolicyafter mTLS is working, since principals depend on mTLS identity (module 05). - Distinct ServiceAccounts are the linchpin. Principal-based authz only
works if each caller runs as its own ServiceAccount (module 05); if
everything uses
default, you can't tell callers apart. SetserviceAccountNameon every Deployment. - Verify each layer before adding the next. After injection,
curlworks and Pods are2/2; after routing, the canary split is observable; after mTLS,authn tls-checksays STRICT and meshed callers still work; after authz, the allow/deny matrix holds; after resilience, fault injection proves the policy. Don't stack an unverified layer on another — that's how you get a failure you can't localise (the whole point of the module-06 review). - Prove, don't assume, the resilience policy. A timeout/retry that's never
been exercised is a guess. Inject the fault, measure the behaviour, then
remove the fault — never leave a
faultblock on your steady-state config (module 07). - Watch the silent-typo class. Every diagnose-and-fix in this track was the same bug shape — a schema-valid selector/label/weight/principal that matches the wrong thing and fails silently (Service selector, injection label, VirtualService order, inverted weight, authz principal). When something "applies cleanly but does nothing," suspect this first and compare the configured string against reality.
- Clean up when done. Delete the namespace (
kubectl delete ns shop) to remove the app; the Istio control plane can stay for future tracks or be removed withistioctl uninstall --purge.
Before you move on
You've now taken a plain-Kubernetes app and given it — without changing a line of application code — encryption and identity on every hop, request-level routing and canaries, identity-based authorization, and provable resilience, all observable in one graph. Just as importantly, you can articulate when this machinery earns its cost and when Ingress + NetworkPolicy + a good client library would have been the wiser, cheaper choice (module 00). That judgement — not the YAML — is what separates "used a mesh once" from "can decide whether to run one."
This track secured and steered stateless service-to-service traffic. The next track turns to the harder problem the mesh deliberately doesn't solve: state. Databases, persistent volumes, StatefulSets, and knowing when to run your own data store versus reaching for a managed Azure database are the subject of 14-databases-and-stateful-workloads — where "just reschedule the Pod" stops being a safe answer and durability becomes the whole game.
Next
14-databases-and-stateful-workloads — run stateful workloads on Kubernetes properly, and know when a managed Azure database is the right call instead.