Getting Started with Istio: A Hands-On Tutorial (2026)

Istio has a reputation for being heavy, and some of that is earned. But you don't need to understand every CRD to get real value out of it. This tutorial walks through the parts that actually matter on day one: getting the mesh installed, proving that mTLS is on, and shifting traffic between two versions of a service. Everything below is meant to be run, not just read.

Istio moves fast, so treat the commands here as a starting shape and verify against current Istio releases. Profile names, default versions, and Gateway API support all shift between minor versions.

If you want the bigger picture of where a mesh fits, start with our service mesh guide. If you're still deciding on a mesh, the Istio vs Linkerd comparison is worth a read before you commit.

Prerequisites #

You need three things:

A running Kubernetes cluster. A local kind or minikube cluster with at least 4 GB of RAM works fine for this. Managed clusters (EKS, GKE, AKS) are all fine too.

kubectl configured and pointing at that cluster. Confirm with kubectl get nodes.

Cluster-admin permissions, since installing Istio creates cluster-scoped resources and CRDs.

That's it. No Helm required for the quick path, though I'll mention it.

Installing Istio #

Download istioctl and put it on your PATH:

bash.bash

curl -L https://istio.io/downloadIstio | sh -
cd istio-*/
export PATH=$PWD/bin:$PATH
istioctl version

Now pick a profile. Istio has two data-plane modes worth knowing about:

Sidecar: the classic model. Every pod gets an Envoy proxy injected next to it. Mature, well understood, and what most tutorials assume.

Ambient: the newer sidecar-less mode. A per-node ztunnel handles L4 and mTLS, and you add waypoint proxies only where you need L7 features. Lower per-pod overhead, but newer.

We'll use the sidecar path here because it's the most portable across versions. Install the demo profile, which turns on the ingress gateway and generous telemetry:

bash.bash

istioctl install --set profile=demo -y
kubectl get pods -n istio-system

You should see istiod and istio-ingressgateway running. If you'd rather manage Istio through GitOps, the Helm charts (istio/base, istiod, istio/gateway) install the same components and are the better fit for production. The Gateway API path uses the same istioctl install but swaps Gateway/HTTPRoute resources for the older ingress gateway. Either works.

Enabling sidecar injection and deploying a sample app #

Injection is opt-in per namespace. Label a namespace and Istio's mutating webhook adds the proxy to every new pod:

bash.bash

kubectl create namespace demo
kubectl label namespace demo istio-injection=enabled

Now deploy the Bookinfo sample that ships with the release. It's a small app with a productpage frontend and three versions of a reviews service, which makes it perfect for a traffic-split demo:

bash.bash

kubectl apply -n demo -f samples/bookinfo/platform/kube/bookinfo.yaml
kubectl get pods -n demo

Watch the READY column. Each pod should show 2/2 once it settles: one container for your app, one for the injected Envoy proxy. If you see 1/1, injection didn't happen, and we'll cover why below.

Verifying automatic mTLS #

Here's the part people don't believe until they see it. Istio turns on mTLS between proxies automatically, in permissive mode by default, so plaintext still works during migration. To require it, apply a PeerAuthentication in STRICT mode:

yaml.yaml

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: demo
spec:
mtls:
mode: STRICT

bash.bash

kubectl apply -f peer-auth.yaml

Now every connection between meshed pods in demo is mutually authenticated and encrypted, and plaintext connections are rejected. You can confirm what a workload negotiated with:

bash.bash

istioctl x describe pod -n demo <productpage-pod-name>

Look for a line reporting that mTLS is enabled. This is the single highest-value thing Istio gives you for the least effort.

Traffic management: a 90/10 canary split #

Bookinfo ships three versions of reviews. Without any rules, Kubernetes load-balances across all of them randomly. Let's take control and send 90% of traffic to v1 and 10% to v2.

First a DestinationRule to name the subsets, then a VirtualService to weight them:

yaml.yaml

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: reviews
namespace: demo
spec:
host: reviews
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: reviews
namespace: demo
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
weight: 90
- destination:
host: reviews
subset: v2
weight: 10

Apply it, then refresh the product page a few dozen times. Roughly one in ten requests should hit v2. To promote the canary, bump v2 to weight: 100 and re-apply. No redeploys, no pod restarts.

If you're on the Gateway API path, the same split is expressed as an HTTPRoute with backendRefs carrying weight fields pointing at two Services. The mechanics are identical; only the resource shape differs.

Viewing telemetry #

The demo profile makes wiring up dashboards trivial. Install the sample addons and open Kiali:

bash.bash

kubectl apply -f samples/addons/
istioctl dashboard kiali

Kiali gives you a live service-graph with request rates, error rates, and a lock icon on edges where mTLS is active. Prometheus scrapes the metrics, and Grafana has prebuilt Istio dashboards (istioctl dashboard grafana) for latency percentiles and throughput per workload. For a first look, drive some traffic to productpage and watch the graph in Kiali light up.

Common gotchas #

Sidecar not injected: the most frequent issue. The namespace label must be present before pods start. If you labeled after deploying, restart the workloads: kubectl rollout restart deployment -n demo.

STRICT mTLS breaking plaintext clients: anything outside the mesh talking directly to a meshed pod on the plaintext port will get rejected under STRICT. Migrate with PERMISSIVE mode first, confirm all clients are meshed, then flip to STRICT.

Resource overhead: each sidecar adds CPU and memory. On dense clusters this adds up. This is exactly the pressure ambient mode is designed to relieve, so evaluate it if per-pod cost is a concern.

Uninstalling cleanly #

Tear down in reverse order so you don't strand resources:

bash.bash

kubectl delete -f samples/addons/ --ignore-not-found
kubectl delete namespace demo
istioctl uninstall --purge -y
kubectl delete namespace istio-system

The --purge flag removes the CRDs too, so only use it when you truly want Istio gone.

Next steps #

Once the basics click, the natural next moves are: fault injection and request timeouts (both live in VirtualService), authorization policies to lock down which services can call which, and multi-cluster mesh if you're spanning regions. If per-pod overhead bothered you, spend an afternoon with ambient mode on a throwaway cluster.

The call we'd make #

For a first install, use the sidecar demo profile, prove mTLS with a STRICT PeerAuthentication, and run one canary split. That combination shows you the two things Istio is genuinely good at, security you didn't have to code and traffic control you didn't have to redeploy for, without drowning you in CRDs. Reach for ambient and the Gateway API path once the fundamentals feel routine, and keep validating every command against the release you actually run.