Istio is a service mesh for Kubernetes: it places an Envoy proxy next to every pod and lets you control traffic, encryption and telemetry between services without changing application code. In this tutorial you will install Istio with istioctl, deploy the Bookinfo sample application with automatic sidecar injection, send a weighted share of traffic to a new version of a service, enforce strict mutual TLS across the mesh and inspect everything in the Kiali dashboard.

Prerequisites

To follow this tutorial, you will need:

  • A running Kubernetes cluster on a version supported by the current Istio release (check the support table at istio.io/latest/docs/releases/supported-releases). A CubePath managed Kubernetes cluster or a self-managed cluster on VPS or bare metal both work.
  • At least 4 vCPUs and 8 GB of RAM free across the worker nodes. The istiod control plane alone requests 500m CPU and 2 GiB of memory.
  • kubectl installed on your workstation and configured for the cluster, with cluster-admin permissions.
  • A Linux or macOS workstation with curl.

Confirm that kubectl can reach the cluster before you start:

kubectl get nodes
NAME       STATUS   ROLES           AGE   VERSION
worker-1   Ready    <none>          12d   v1.33.4
worker-2   Ready    <none>          12d   v1.33.4
worker-3   Ready    <none>          12d   v1.33.4

Step 1 - Downloading istioctl

istioctl is the command-line tool that installs, upgrades and inspects Istio. The official download script fetches the latest release, including istioctl and the sample manifests used later in this tutorial.

Download the script, read it, and then run it:

curl -L https://istio.io/downloadIstio -o downloadIstio.sh
less downloadIstio.sh
sh downloadIstio.sh

The script creates a directory named istio-<version>. Change into it and add its bin folder to your PATH for the current shell:

cd istio-*/
export PATH="$PWD/bin:$PATH"

Stay in this directory: the samples/ folder is used in the next steps. Check the client version:

istioctl version --remote=false
client version: 1.27.1

Your version number will likely be newer. Before installing, run the pre-flight check against the cluster:

istioctl x precheck
✔ No issues found when checking the cluster. Istio is safe to install or upgrade!

Step 2 - Installing the Istio control plane

Istio ships several configuration profiles. The default profile installs istiod and an ingress gateway and is the one recommended for production. The demo profile enables extra logging and lower resource requests and is meant for testing only.

Install the default profile. istioctl creates the istio-system namespace for you:

istioctl install --set profile=default -y
✔ Istio core installed
✔ Istiod installed
✔ Ingress gateways installed
✔ Installation complete

Verify that the control plane and the gateway are running:

kubectl get pods -n istio-system
NAME                                    READY   STATUS    RESTARTS   AGE
istio-ingressgateway-6b8c5d8f9d-7kq2x   1/1     Running   0          60s
istiod-7d4f9c6b8b-l2m9p                 1/1     Running   0          75s

If you want to customize the installation later (resources, access logs, gateways), write an IstioOperator file and apply it with istioctl install -f your_file.yaml -y instead of passing many --set flags.

Step 3 - Enabling sidecar injection and deploying Bookinfo

Istio adds the Envoy sidecar to pods through a mutating webhook. The webhook only acts on namespaces carrying the istio-injection=enabled label, and only on pods created after the label is set.

Label the default namespace, where you will deploy the sample application:

kubectl label namespace default istio-injection=enabled

Deploy Bookinfo, a small application made of four services (productpage, details, reviews and ratings). The reviews service runs in three versions, labeled version: v1, v2 and v3:

kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml

Wait until all pods are ready, then list them:

kubectl wait --for=condition=Ready pods --all -n default --timeout=180s
kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
details-v1-65cfcf56f9-4x9zq       2/2     Running   0          70s
productpage-v1-d5789fdfb-6nq7d    2/2     Running   0          70s
ratings-v1-7c9bd4b87f-8m2kc       2/2     Running   0          70s
reviews-v1-6584ddcf65-rkq5z       2/2     Running   0          70s
reviews-v2-6f85cb9b7c-9hxwt       2/2     Running   0          70s
reviews-v3-6f5b775685-5bt2v       2/2     Running   0          70s

READY 2/2 means each pod runs the application container plus the istio-proxy sidecar. Confirm the application responds from inside the mesh:

kubectl exec "$(kubectl get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}')" -c ratings -- curl -sS productpage:9080/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>

Step 4 - Exposing the application through the ingress gateway

Traffic from outside the cluster enters the mesh through the Istio ingress gateway. The Bookinfo sample includes a Gateway that listens on port 80 and a VirtualService that routes /productpage and related paths to the productpage service:

kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml

Check the external address of the gateway:

kubectl get svc istio-ingressgateway -n istio-system

If your cluster has a load balancer integration, EXTERNAL-IP shows a public address and you can open http://EXTERNAL-IP/productpage in a browser. If it stays <pending>, forward a local port to the gateway for testing:

kubectl port-forward -n istio-system svc/istio-ingressgateway 8080:80

In a second terminal, request the page:

curl -s http://localhost:8080/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>

Reload http://localhost:8080/productpage in a browser a few times. Because there are no routing rules yet, requests to reviews are spread across v1 (no stars), v2 (black stars) and v3 (red stars).

Step 5 - Routing traffic with DestinationRules and VirtualServices

Two resources control routing inside the mesh:

  • A DestinationRule defines named subsets of a service, usually by the version label, and traffic policies such as connection limits.
  • A VirtualService decides which subset receives each request.

Create a file for the reviews routing rules:

nano reviews-routing.yaml

Add the following content. The DestinationRule defines subsets v1 and v2 and adds outlier detection, which temporarily removes a pod from the load balancing pool after five consecutive 5xx errors (a basic circuit breaker). The VirtualService sends 90% of requests to v1 and 10% to v2:

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  trafficPolicy:
    connectionPool:
      http:
        http1MaxPendingRequests: 100
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
      weight: 90
    - destination:
        host: reviews
        subset: v2
      weight: 10
    timeout: 5s
    retries:
      attempts: 2
      perTryTimeout: 2s
      retryOn: 5xx,connect-failure

Apply it:

kubectl apply -f reviews-routing.yaml

Let istioctl validate the configuration in the namespace:

istioctl analyze
✔ No validation issues found when analyzing namespace: default.

Now reload the product page several times. Most responses show no stars (v1), about one in ten shows black stars (v2), and red stars (v3) no longer appear. To complete a canary rollout, raise the v2 weight step by step and apply the file again each time until v2 receives 100%.

Step 6 - Enforcing strict mutual TLS

By default Istio runs in permissive mode: sidecars encrypt traffic between each other with mutual TLS but still accept plain-text connections from workloads outside the mesh. Strict mode rejects any connection that is not mutual TLS.

First create a client outside the mesh, in a namespace without the injection label, to prove the difference:

kubectl create namespace legacy
kubectl run curl -n legacy --image=curlimages/curl --restart=Never --command -- sleep 3600
kubectl wait --for=condition=Ready pod/curl -n legacy --timeout=60s

Request the product page from this pod. In permissive mode it succeeds:

kubectl exec -n legacy curl -- curl -s -o /dev/null -w "%{http_code}\n" http://productpage.default:9080/productpage
200

Create a mesh-wide PeerAuthentication policy. A policy named default in the istio-system root namespace applies to the whole mesh:

nano mtls-strict.yaml
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT

Apply it:

kubectl apply -f mtls-strict.yaml

Repeat the request from the legacy namespace:

kubectl exec -n legacy curl -- curl -s -o /dev/null -w "%{http_code}\n" http://productpage.default:9080/productpage
000
command terminated with exit code 56

The connection is reset because the client has no sidecar and cannot present a mesh certificate. Requests between Bookinfo services and through the ingress gateway keep working, since all of them run Envoy.

Step 7 - Visualizing the mesh with Kiali

Kiali draws the service graph from Istio metrics stored in Prometheus. The Istio release includes sample manifests for both. They are fine for learning; for production, run your own Prometheus and install Kiali with its operator or Helm chart.

Install the addons from the release directory:

kubectl apply -f samples/addons/prometheus.yaml
kubectl apply -f samples/addons/kiali.yaml
kubectl rollout status deployment/kiali -n istio-system
deployment "kiali" successfully rolled out

Generate some traffic so the graph has data. With the port-forward from Step 4 still running, run:

for i in $(seq 1 100); do curl -s -o /dev/null http://localhost:8080/productpage; done

Open the dashboard. istioctl forwards a local port and opens your browser:

istioctl dashboard kiali

In Kiali, open Traffic Graph, select the default namespace and enable the Security display option. You will see the path from istio-ingressgateway to productpage and on to reviews, with roughly 90% of reviews traffic going to v1, and a padlock on each edge showing that mutual TLS is in use.

Troubleshooting

  • Pods show 1/1 instead of 2/2. The pods were created before the namespace was labeled, or the label is missing. Check with kubectl get namespace default --show-labels, then recreate the pods with kubectl rollout restart deployment -n default.
  • istioctl analyze reports ReferencedResourceNotFound for a subset. The VirtualService points to a subset that no DestinationRule defines, or the subset labels match no pods. Compare the subset labels with kubectl get pods --show-labels.
  • Requests return 503 after applying a VirtualService. Usually the same subset mismatch as above. Check the sidecar logs with kubectl logs deploy/productpage-v1 -c istio-proxy.
  • The ingress gateway EXTERNAL-IP stays <pending>. The cluster has no load balancer provider. Use a NodePort service, a bare-metal load balancer such as MetalLB, or put an external load balancer in front of the node ports.

Conclusion

You installed Istio with the default profile, added Envoy sidecars to an application, shifted a controlled share of traffic to a new version with a DestinationRule and a VirtualService, enforced strict mutual TLS and inspected the result in Kiali. From here, you can restrict which services may call each other with AuthorizationPolicy resources, expose HTTPS on the ingress gateway with certificates from cert-manager, or evaluate Istio's ambient mode, which removes the per-pod sidecar.