One of the most common next steps after setting up a Kubernetes cluster is adding a service mesh. Istio is the most widely adopted service mesh in the CNCF ecosystem — it gives you automatic mTLS encryption, traffic routing, observability, and resilience features without changing a single line of application code.
This guide walks through the full workflow:
- Installing
istioctland the Istio control plane (default profile) - Enabling automatic Envoy sidecar injection
- Deploying and exploring the Bookinfo sample application
- Exposing the app on a bare-metal / single-node cluster (no cloud LoadBalancer required)
- Visualizing your service mesh with the Kiali dashboard
- Testing traffic routing with VirtualServices and DestinationRules
Official reference: Istio Installation Guide
Configuration Placeholders
| Placeholder | Description | Example |
|---|---|---|
<NODE_IP> | IP address of your Kubernetes node | 192.168.1.100 |
<NODE_PORT> | NodePort assigned by Kubernetes | 32192 |
<ISTIO_VERSION> | Istio release version downloaded | 1.31.0 |
Environment Specifications
| Component | Details |
|---|---|
| Kubernetes | Single-node cluster provisioned with kubeadm |
| CNI | Calico |
| Istio Version | 1.31.x |
| Install Method | istioctl (CLI binary) |
| Profile | default |
Note: Since this is a bare-metal single-node cluster without a cloud LoadBalancer (not GKE/EKS/AKS), services of type
LoadBalancercreated by the Istio IngressGateway will never receive anEXTERNAL-IP. This guide includes alternative access methods viaport-forwardandNodePort.
Prerequisites
Before starting, make sure the following are in place:
- A single-node Kubernetes cluster in
Readystate (see Kubernetes Single-Node Setup on AlmaLinux 9) kubectlconfigured and able to access the cluster (kubectl get nodesshowsReady)- Sufficient resources on the node to run the Istio control plane + 4 Bookinfo microservices (at least 2 CPU / 4 GB RAM free is recommended)
- Internet access from the node (to download the Istio binary and pull container images)
Step 1: Download and Install istioctl
1.1 Download the Istio Release
curl -L https://istio.io/downloadIstio | sh -Output:
Downloading istio-<ISTIO_VERSION> from https://github.com/istio/istio/releases/download/<ISTIO_VERSION>/istio-<ISTIO_VERSION>-linux-amd64.tar.gz ...
Istio <ISTIO_VERSION> download complete!
The Istio release archive has been downloaded to the istio-<ISTIO_VERSION> directory.
To configure the istioctl client tool for your workstation,add the /home/user/istio-<ISTIO_VERSION>/bin directory to your environment path variable with: export PATH="$PATH:/home/user/istio-<ISTIO_VERSION>/bin"
Begin the Istio pre-installation check by running: istioctl x precheckNavigate into the extracted directory:
cd istio-*/1.2 Add istioctl to PATH
export PATH=$PWD/bin:$PATH
# To make it permanent, append to your shell profileecho 'export PATH=$HOME/istio-<ISTIO_VERSION>/bin:$PATH' >> ~/.bashrcTip: Replace
<ISTIO_VERSION>with the actual version that was downloaded (e.g.,1.31.0).
1.3 Verify istioctl
istioctl versionOutput:
Istio is not present in the cluster: no running Istio pods in namespace "istio-system"1.31.0The first line is expected — Istio hasn’t been installed on the cluster yet. The important thing is that the
istioctlclient version is printed.
Step 2: Install Istio with the Default Profile
2.1 Pre-installation Check (optional but recommended)
istioctl x precheckOutput:
✔ No issues found when checking the cluster. Istio is safe to install or upgrade! To get started, check out https://istio.io/latest/docs/setup/getting-started/.Make sure there are no critical warnings or errors before proceeding.
2.2 Install the Default Profile
istioctl install --set profile=default -yThis command installs:
istiod(the control plane) in theistio-systemnamespaceistio-ingressgatewayas the default ingress gateway
Output:
✔ Istio core installed ⛵️✔ Istiod installed 🧠✔ Ingress gateways installed 🛬✔ Installation completeStep 3: Verify the Installation
3.1 Check Pods in istio-system
kubectl get pods -n istio-systemOutput:
NAME READY STATUS RESTARTS AGEistio-ingressgateway-6dcdbcd758-z6mtb 1/1 Running 0 2m10sistiod-84568cdf7c-tv9mt 1/1 Running 0 2m24s3.2 Check Control Plane Version
istioctl versionOutput:
client version: 1.31.0control plane version: 1.31.0data plane version: 1.31.0 (1 proxies)Now istioctl can read both the client and control plane versions.
3.3 Analyze Configuration (optional)
istioctl analyze -AUse this to detect potential configuration issues across all namespaces before deploying any applications.
Step 4: Enable Automatic Sidecar Injection
Istio automatically injects an Envoy sidecar proxy into every pod deployed in namespaces labeled with istio-injection=enabled.
kubectl label namespace default istio-injection=enabledVerify the label:
kubectl get namespace -L istio-injectionOutput:
NAME STATUS AGE ISTIO-INJECTIONdefault Active 4h29m enabledistio-system Active 3m26skube-system Active 4h29m...Step 5: Deploy the Bookinfo Application
Bookinfo is Istio’s official sample application, consisting of 4 microservices (productpage, details, reviews, ratings) with 3 different versions of the reviews service (v1, v2, v3) — perfect for demonstrating traffic routing capabilities.
5.1 Apply the Bookinfo Manifest
kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yamlMake sure you are still inside the extracted Istio directory (
istio-<ISTIO_VERSION>/) so that thesamples/bookinfo/...path resolves correctly.
5.2 Verify Services and Pods
kubectl get servicesOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEdetails ClusterIP 10.102.162.188 <none> 9080/TCP 7skubernetes ClusterIP 10.96.0.1 <none> 443/TCP 4h30mproductpage ClusterIP 10.111.175.168 <none> 9080/TCP 7sratings ClusterIP 10.108.180.245 <none> 9080/TCP 7sreviews ClusterIP 10.99.206.200 <none> 9080/TCP 7skubectl get podsMake sure each pod shows 2/2 in the READY column — the 2 indicates that the Envoy sidecar was successfully injected alongside the application container.
Output:
NAME READY STATUS RESTARTS AGEdetails-v1-65bd58d55f-svxqk 2/2 Running 0 7m32sproductpage-v1-7498b5bd44-4s76z 2/2 Running 0 7m32sratings-v1-9bff6cb79-7zpjv 2/2 Running 0 7m32sreviews-v1-54465994f9-47h5d 2/2 Running 0 7m32sreviews-v2-577579b4c5-6vvfl 2/2 Running 0 7m32sreviews-v3-6d7944df59-tlr6r 2/2 Running 0 7m32sIf the
READYcolumn shows 1/1, the sidecar was not injected. Double-check the namespace label from Step 4, then restart the deployments withkubectl rollout restart deployment.
5.3 Test from Inside the Cluster
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>"Output:
<title>Simple Bookstore App</title>5.4 Create the Istio Gateway
kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yamlOutput:
gateway.networking.istio.io/bookinfo-gateway createdvirtualservice.networking.istio.io/bookinfo createdVerify:
kubectl get gatewayOutput:
NAME AGEbookinfo-gateway 24sStep 6: Expose Bookinfo on a Single-Node Cluster
This is where the setup diverges from the official Istio documentation, which assumes your cluster has a cloud LoadBalancer. On a bare-metal single-node cluster, istio-ingressgateway will remain stuck in Pending:
kubectl get svc istio-ingressgateway -n istio-systemOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEistio-ingressgateway LoadBalancer 10.100.98.219 <pending> 15021:32283/TCP,80:32192/TCP,443:32707/TCP 12mChoose one of the options below to access Bookinfo from outside the cluster.
Option A — Port Forward (fastest for demos)
kubectl port-forward -n istio-system svc/istio-ingressgateway 8080:80Keep this terminal running, then access the app from your browser or via curl:
http://<NODE_IP>:8080/productpageIf accessing from the server itself, use
http://localhost:8080/productpage. If accessing from another machine (e.g., your laptop), make sure port 8080 is open in the firewall and use the server’s IP address.
Option B — Patch to NodePort (more permanent)
kubectl patch svc istio-ingressgateway -n istio-system -p '{"spec": {"type": "NodePort"}}'Check the NodePort assigned to port 80:
kubectl get svc istio-ingressgateway -n istio-system -o jsonpath='{.spec.ports[?(@.port==80)].nodePort}'Then access via:
http://<NODE_IP>:<NODE_PORT>/productpageOnce accessible, you should see the Bookinfo product page:

Step 7: Confirm the App is Working
Use curl to verify the application is accessible (example using port-forward from Option A):
curl -s "http://localhost:8080/productpage" | grep -o "<title>.*</title>"Output:
<title>Simple Bookstore App</title>Open the page in your browser and refresh several times — you’ll notice the reviews section cycling through different versions in round-robin fashion: no stars, black stars, and red stars. This happens because no routing rules have been applied yet, so traffic is distributed equally across all three versions.

Step 8: Define Service Versions with DestinationRules
Before configuring traffic routing between reviews versions (e.g., request routing or traffic shifting), Istio needs to know which subsets/versions are available. This is done through DestinationRule resources.
kubectl apply -f samples/bookinfo/networking/destination-rule-all.yamlOutput:
destinationrule.networking.istio.io/productpage createddestinationrule.networking.istio.io/reviews createddestinationrule.networking.istio.io/ratings createddestinationrule.networking.istio.io/details createdVerify:
kubectl get destinationrules -o yamlThe
defaultprofile already enables auto mutual TLS, so you don’t need to set up mTLS manually for this demo.
Step 9: Kiali Dashboard — Visualizing Your Service Mesh
Kiali is the official observability console for Istio. It provides a real-time visual representation of your service mesh topology, traffic flow, health status, and configuration validation. Istio ships Kiali as part of a bundle of observability addons alongside Prometheus (metrics), Grafana (dashboards), and Jaeger (distributed tracing).
9.1 Install the Observability Addons
Install all addons included in the Istio samples directory:
kubectl apply -f samples/addonsOutput:
serviceaccount/grafana createdconfigmap/grafana createdservice/grafana createddeployment.apps/grafana createdconfigmap/istio-grafana-dashboards createdconfigmap/istio-services-grafana-dashboards createddeployment.apps/jaeger createdservice/tracing createdserviceaccount/kiali createdconfigmap/kiali createdclusterrole.rbac.authorization.k8s.io/kiali createdclusterrolebinding.rbac.authorization.k8s.io/kiali createdservice/kiali createddeployment.apps/kiali createdserviceaccount/prometheus createdconfigmap/prometheus createdservice/prometheus createddeployment.apps/prometheus created...9.2 Wait for Kiali to Be Ready
kubectl rollout status deployment/kiali -n istio-systemOutput:
Waiting for deployment "kiali" rollout to finish: 0 of 1 updated replicas are available...deployment "kiali" successfully rolled out9.3 Access the Kiali Dashboard
To access the dashboard, patch the Kiali service to NodePort — the same approach used to expose the IngressGateway:
kubectl patch svc kiali -n istio-system -p '{"spec": {"type": "NodePort"}}'Check the NodePort assigned to port 20001:
kubectl get svc kiali -n istio-system -o jsonpath='{.spec.ports[?(@.port==20001)].nodePort}'Then open the dashboard in your browser at http://<NODE_IP>:<NODE_PORT>:

Navigate to the Graph section — Kiali displays a live visualization of your service mesh, showing the relationships between services in the Bookinfo application. It also provides filters to help you analyze traffic flow across the mesh.

Step 10: Test Traffic Routing with VirtualServices
Since the DestinationRules (with subsets v1/v2/v3) were applied in the previous step, you can now create a VirtualService to route 100% of traffic to a specific version. Let’s route all reviews traffic to v2 (black stars):
cat <<EOF | kubectl apply -f -apiVersion: networking.istio.io/v1kind: VirtualServicemetadata: name: reviewsspec: hosts: - reviews http: - route: - destination: host: reviews subset: v2EOFVerify the routing rule:
kubectl get virtualservice reviews -o yamlRefresh /productpage several times — now the reviews section will always show black stars (v2 fetches rating data but displays stars in black, unlike v3 which shows red stars). No more round-robin cycling between versions.

You can also confirm this in the Kiali graph — traffic to the reviews service now flows exclusively to the v2 subset, with no traffic going to v1 or v3:

Cleanup
Once you’re done exploring, clean up all the resources:
# Remove Bookinfo and its gatewaysamples/bookinfo/platform/kube/cleanup.sh
# (Optional) Revert services back to ClusterIP if you patched them to NodePortkubectl patch svc istio-ingressgateway -n istio-system -p '{"spec": {"type": "ClusterIP"}}'
# Uninstall Istio from the clusteristioctl uninstall --purge -y
# Delete the istio-system namespacekubectl delete namespace istio-systemTroubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
| Sidecar not injected (pods show 1/1 instead of 2/2) | Namespace missing istio-injection label | Run kubectl get namespace default --show-labels and re-apply the label, then kubectl rollout restart deployment -n default |
istio-ingressgateway stuck in <pending> | No cloud LoadBalancer on bare-metal | Use Option A (port-forward) or Option B (NodePort) from Step 6 |
productpage returns 503 / upstream connect error | Sidecar not fully ready or DestinationRules not applied | Check kubectl get pods -n default and inspect logs with kubectl logs <pod> -c istio-proxy |
istiod in CrashLoopBackOff | Insufficient CPU/RAM on the node | Check kubectl describe pod -n istio-system -l app=istiod and kubectl top nodes |