Technical Insight How-to Guide

How to Set Up Istio Service Mesh in Kubernetes

A practical step-by-step guide to installing Istio in Kubernetes using the sidecar data plane, enabling mTLS, traffic routing, canary deployments, retries, circuit breaking, authorization, and observability.

Service mesh with Istio

Technical Context

Where this fits.

Knowledge area Kubernetes Section Operations Purpose How-to Guide

Istio is a Kubernetes service mesh providing traffic management, workload identity, mutual TLS, authorization, observability, and advanced resiliency capabilities.

In this guide we’ll install Istio using its traditional sidecar data plane, add an application namespace to the mesh, enforce mTLS, configure traffic routing, perform a canary rollout, configure retries and circuit breaking, and introduce service-to-service authorization.

By the end, our application traffic will look roughly like this:

Diagram source
flowchart LR
    subgraph Kubernetes
        subgraph PodA["Frontend Pod"]
            A[Frontend] --> EA[Envoy]
        end

        subgraph PodB["API Pod"]
            EB[Envoy] --> B[API]
        end

        EA <-->|mTLS| EB

        CP[Istiod]
        CP -.->|Configuration| EA
        CP -.->|Configuration| EB
    end

The application containers do not need to implement the service-mesh functionality themselves.

Envoy handles it on their behalf.

Sidecar Mode and Ambient Mode

Modern Istio supports two data-plane architectures:

Sidecar mode
    Pod
    ├── Application
    └── Envoy

Ambient mode
    Pod
    └── Application

    Node
    └── ztunnel

    Optional L7
    └── waypoint proxy

This article deliberately uses sidecar mode.

It remains useful when:

  • you already operate an Envoy-sidecar Istio environment
  • you want the traditional Istio architecture
  • workloads depend on sidecar-specific behavior
  • you need predictable per-workload L7 proxying
  • you’re learning the relationship between Istio traffic resources and Envoy

Ambient mode removes the requirement to inject an Envoy proxy into every application pod and is worth evaluating for new environments.

We’ll return to it at the end.

Prerequisites

You need:

  • a Kubernetes cluster
  • kubectl
  • cluster-admin or equivalent installation permissions
  • a supported Kubernetes version
  • sufficient resources for the Istio control plane and Envoy proxies

Verify the cluster:

kubectl cluster-info
kubectl get nodes

All nodes should be healthy before introducing a service mesh.

We’ll use this application namespace throughout the guide:

kubectl create namespace production

Install istioctl

istioctl is the primary Istio administration and diagnostic CLI.

On macOS:

brew install istioctl

Alternatively, download an Istio release:

curl -L https://istio.io/downloadIstio | sh -

Enter the downloaded directory:

cd istio-*

Add the CLI to your path:

export PATH=$PWD/bin:$PATH

Verify it:

istioctl version

Run the Pre-Installation Check

Before installing Istio:

istioctl x precheck

Resolve any reported problems before proceeding.

Typical areas include:

  • Kubernetes compatibility
  • permissions
  • webhook support
  • required APIs
  • platform configuration

Choose an Istio Installation Profile

Istio provides several installation profiles.

Common profiles include:

default
demo
minimal
ambient
remote

For this guide we’ll use:

default

The default profile is the normal starting point for a production sidecar deployment.

The demo profile is useful for evaluation and tutorials but enables settings that are not intended as a production baseline.

The minimal profile only installs the control-plane components and is useful when building a more explicitly separated topology.

Install Istio

Install the default profile:

istioctl install -y

Or explicitly:

istioctl install \
  --set profile=default \
  -y

Check the control plane:

kubectl get pods -n istio-system

Verify the installation:

istioctl verify-install

Then:

istioctl proxy-status

There will not yet be application proxies until workloads are added to the mesh.

Enable Automatic Sidecar Injection

Enable injection on the production namespace:

kubectl label namespace production \
  istio-injection=enabled

Verify it:

kubectl get namespace production \
  --show-labels

Existing pods are not modified.

Restart the deployments:

kubectl rollout restart deployment \
  -n production

Watch the rollout:

kubectl get pods -n production -w

Verify Envoy Injection

Before injection, a pod may show:

READY
1/1

After injection:

READY
2/2

Inspect the containers:

kubectl get pod <pod-name> \
  -n production \
  -o jsonpath='{.spec.containers[*].name}'

You should see:

application istio-proxy

istio-proxy is the Envoy sidecar.

You can also inspect its configuration:

istioctl proxy-config all \
  <pod-name> \
  -n production

Understand Istio mTLS

Istio automatically uses mTLS when traffic is exchanged between workloads that support Istio mTLS.

Conceptually:

Diagram source
sequenceDiagram
    participant A as Frontend
    participant EA as Envoy A
    participant EB as Envoy B
    participant B as API

    A->>EA: HTTP
    EA->>EB: mTLS
    EB->>B: HTTP
    B->>EB: HTTP
    EB->>EA: mTLS
    EA->>A: HTTP

However, there is an important distinction between:

using mTLS

and:

requiring mTLS

By default, Istio supports incremental mesh adoption. A meshed workload can therefore accept both mTLS and plaintext connections under permissive behavior.

To prevent plaintext traffic, configure STRICT peer authentication.

Enforce Strict mTLS

Create:

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

Apply it:

kubectl apply -f peer-authentication.yaml

Now workloads in production require incoming connections to use Istio mTLS.

A non-meshed workload attempting to directly access a protected workload should therefore fail.

During a gradual migration, keep workloads permissive until all required clients participate in the mesh.

Define Application Versions

Suppose our API has two versions:

api-v1
api-v2

Both deployments are selected by the Kubernetes Service:

apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: production
spec:
  selector:
    app: api

  ports:
    - port: 80
      targetPort: 8080

The deployments use labels:

app: api
version: v1

and:

app: api
version: v2

Istio can use those labels to create logical subsets.

Create an Istio DestinationRule

A DestinationRule defines policies applied after Istio has selected a destination.

Create:

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: api
  namespace: production

spec:
  host: api

  subsets:
    - name: stable
      labels:
        version: v1

    - name: canary
      labels:
        version: v2

Apply it:

kubectl apply -f destination-rule.yaml

We can now route traffic to either the stable or canary subset without creating separate public endpoints.

Configure a Canary Deployment

Create a VirtualService:

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api
  namespace: production

spec:
  hosts:
    - api

  http:
    - route:
        - destination:
            host: api
            subset: stable
          weight: 90

        - destination:
            host: api
            subset: canary
          weight: 10

Apply it:

kubectl apply -f virtual-service.yaml

Traffic now looks approximately like:

               +--> v1 stable  90%
frontend -> api
               +--> v2 canary  10%

This makes traffic rollout independent of Kubernetes replica counts.

Gradually Shift Traffic

A release might progress through:

100 / 0
99 / 1
90 / 10
75 / 25
50 / 50
0 / 100

You only need to modify the VirtualService weights.

This gives platform and application teams fine-grained control over release exposure.

Header-Based Routing

Istio can also route requests based on HTTP attributes.

For example, requests containing:

x-canary: true

can be sent entirely to the new version:

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api
  namespace: production

spec:
  hosts:
    - api

  http:
    - match:
        - headers:
            x-canary:
              exact: "true"

      route:
        - destination:
            host: api
            subset: canary

    - route:
        - destination:
            host: api
            subset: stable
          weight: 90

        - destination:
            host: api
            subset: canary
          weight: 10

Developers or automated tests can explicitly exercise the canary:

curl \
  -H "x-canary: true" \
  http://api.production

while ordinary users continue through the weighted rollout.

Configure Request Timeouts

Slow dependencies can cause resource exhaustion upstream.

Set a request timeout:

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api
  namespace: production

spec:
  hosts:
    - api

  http:
    - route:
        - destination:
            host: api

      timeout: 10s

Requests taking longer than ten seconds fail rather than waiting indefinitely.

Timeouts should reflect the application’s actual latency requirements rather than being copied blindly between services.

Configure Retries

Transient errors can sometimes be retried automatically:

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api
  namespace: production

spec:
  hosts:
    - api

  http:
    - route:
        - destination:
            host: api

      timeout: 10s

      retries:
        attempts: 2
        perTryTimeout: 3s
        retryOn: 5xx,reset,connect-failure

Retries should be used conservatively.

In particular, be careful with non-idempotent operations such as:

POST /payments
POST /orders
POST /transactions

unless the application implements appropriate idempotency semantics.

Configure Connection Pools

Istio’s DestinationRule can control connection behavior:

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: api
  namespace: production

spec:
  host: api

  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100

      http:
        http1MaxPendingRequests: 100
        http2MaxRequests: 1000

  subsets:
    - name: stable
      labels:
        version: v1

    - name: canary
      labels:
        version: v2

Connection pools help prevent a caller from creating unbounded pressure on a downstream service.

Configure Outlier Detection

Istio can detect failing endpoints and temporarily remove them from load balancing.

For example:

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: api
  namespace: production

spec:
  host: api

  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 50

Conceptually:

api-1   healthy  -> receive traffic
api-2   healthy  -> receive traffic
api-3   failing  -> temporarily ejected

This can reduce the impact of a malfunctioning instance before Kubernetes itself decides the pod is unhealthy.

Circuit Breaking

Circuit breaking is not a single Istio switch.

It is typically implemented through a combination of:

  • connection limits
  • pending request limits
  • request limits
  • outlier detection

For example:

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: api
  namespace: production

spec:
  host: api

  trafficPolicy:

    connectionPool:
      tcp:
        maxConnections: 100

      http:
        http1MaxPendingRequests: 50

    outlierDetection:
      consecutive5xxErrors: 3
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 50

This protects the caller and destination from several failure modes.

Service-to-Service Authorization

mTLS gives workloads authenticated identities.

Authorization policies let us decide what those identities may access.

Suppose:

frontend -> api

should be allowed, while arbitrary workloads should not be able to call the API.

Assume the frontend runs using:

ServiceAccount: frontend
Namespace: production

Create:

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: api
  namespace: production

spec:
  selector:
    matchLabels:
      app: api

  action: ALLOW

  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/production/sa/frontend

Once an ALLOW policy selects the API workload, requests not matching an allow rule are denied.

This changes security from:

Anything with network connectivity can call the API

to:

Only explicitly authorized workload identities can call the API

Restrict HTTP Operations

Authorization can be made more granular:

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: api
  namespace: production

spec:
  selector:
    matchLabels:
      app: api

  action: ALLOW

  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/production/sa/frontend

      to:
        - operation:
            methods:
              - GET
              - POST

            paths:
              - "/api/v1/*"

The frontend identity can now access only selected HTTP operations.

For identity-based authorization like this, use strict mTLS so that source identity is reliably established.

Install Observability Components

Istio produces telemetry that can be consumed by systems such as:

  • Prometheus
  • Grafana
  • Kiali
  • Jaeger
  • OpenTelemetry

For a test environment, the Istio repository includes sample add-ons.

For example, using manifests that match your installed Istio release:

kubectl apply -f samples/addons/prometheus.yaml
kubectl apply -f samples/addons/kiali.yaml
kubectl apply -f samples/addons/jaeger.yaml

These sample manifests are useful for learning and demonstrations.

They should not be treated as production deployment manifests.

For production, integrate Istio telemetry with your managed observability stack or deploy the individual observability systems using their supported production installation mechanisms.

Open Kiali

If Kiali is installed:

istioctl dashboard kiali

Kiali provides a topology-oriented view of the mesh.

You can inspect:

  • services
  • workloads
  • request rates
  • error rates
  • latency
  • traffic relationships
  • Istio configuration

A service graph is particularly useful for identifying dependencies that may not be documented elsewhere.

Inspect Prometheus Metrics

Envoy exposes Prometheus-compatible telemetry.

Common service-mesh signals include:

Request rate
Error rate
P50 latency
P95 latency
P99 latency
TCP connections
Bytes transferred

Avoid retaining every high-cardinality Envoy metric indefinitely. Large Istio installations can produce substantial telemetry volume.

Distributed Tracing

A service mesh can provide telemetry around requests, but tracing still depends on appropriate trace context propagation through the application.

A typical request might look like:

Diagram source
flowchart LR
    U[User]
    --> I[Ingress]
    --> F[Frontend]
    --> A[API]
    --> D[Database]

A distributed trace allows the individual spans to be correlated into a single request path.

If Jaeger is installed for testing:

istioctl dashboard jaeger

For production environments, consider integrating Istio with your existing OpenTelemetry tracing architecture.

Troubleshooting Istio

Istio has extensive diagnostic tooling.

Start with:

istioctl analyze

Or scope it:

istioctl analyze -n production

This catches many configuration problems before you start inspecting Envoy directly.

Check Proxy Synchronization

Run:

istioctl proxy-status

This shows whether Envoy proxies are synchronized with istiod.

A proxy that remains stale or disconnected can behave differently from the rest of the mesh.

Inspect Envoy Configuration

To inspect all generated configuration:

istioctl proxy-config all \
  deploy/api \
  -n production

More targeted commands are often easier to read.

Clusters:

istioctl proxy-config clusters \
  deploy/api \
  -n production

Listeners:

istioctl proxy-config listeners \
  deploy/api \
  -n production

Routes:

istioctl proxy-config routes \
  deploy/api \
  -n production

Endpoints:

istioctl proxy-config endpoints \
  deploy/api \
  -n production

These commands are invaluable when the Kubernetes configuration looks correct but Envoy is not behaving as expected.

Inspect Envoy Logs

kubectl logs deploy/api \
  -n production \
  -c istio-proxy

Check the application container separately so that proxy failures are not confused with application failures.

Validate Traffic Configuration

Run:

istioctl analyze -n production

Then inspect:

kubectl get virtualservice -n production
kubectl get destinationrule -n production
kubectl get peerauthentication -n production
kubectl get authorizationpolicy -n production

A useful troubleshooting sequence is:

Kubernetes Service
        ↓
Endpoints
        ↓
DestinationRule
        ↓
VirtualService
        ↓
Envoy configuration
        ↓
Authentication / Authorization

Do not start by reading thousands of lines of Envoy logs if the Kubernetes Service has no endpoints.

Production Installation Considerations

For production, treat Istio as part of the platform rather than as an application add-on.

Version control:

  • installation profile
  • Istio version
  • platform-specific settings
  • ingress/egress gateway configuration
  • resource requests and limits
  • PeerAuthentication
  • AuthorizationPolicy
  • VirtualService
  • DestinationRule
  • telemetry configuration

Also define an upgrade procedure before deploying broadly.

Service meshes touch almost every workload in the cluster, so uncontrolled upgrades carry a larger blast radius than ordinary application upgrades.

Roll Out Istio Gradually

Do not start by injecting every namespace.

A safer migration is:

Diagram source
flowchart LR
    A[Install Istio]
    --> B[Select Test Namespace]
    --> C[Enable Injection]
    --> D[Verify Traffic]
    --> E[Observe mTLS]
    --> F[Enable STRICT mTLS]
    --> G[Add Authorization]
    --> H[Expand Rollout]

Start with:

kubectl label namespace test \
  istio-injection=enabled

Restart only those workloads.

Observe them before proceeding namespace by namespace.

This makes it much easier to distinguish application problems from mesh-related problems.

What About Istio Ambient Mode?

This guide intentionally uses sidecars, but a new Istio deployment should also evaluate Ambient mode.

With traditional Istio:

Pod
├── Application
└── Envoy

With Ambient:

Pod
└── Application

Node
└── ztunnel

Optional L7 functionality can be provided through waypoint proxies.

Ambient therefore changes the operational model substantially.

Instead of injecting and sizing an Envoy proxy for every application pod, the base secure connectivity layer is provided by node-level ztunnel components.

The installation path is also different:

istioctl install \
  --set profile=ambient \
  --skip-confirmation

Ambient also makes extensive use of Kubernetes Gateway API resources.

Do not mix installation instructions between the two architectures without understanding the resulting data-plane topology.

Final Verification

Run:

istioctl analyze

Then:

istioctl proxy-status

Verify the namespace:

kubectl get pods -n production

Verify security configuration:

kubectl get peerauthentication -n production
kubectl get authorizationpolicy -n production

Verify traffic configuration:

kubectl get virtualservice -n production
kubectl get destinationrule -n production

At this point the mesh provides the foundation for:

  • workload identity
  • automatic mTLS
  • strict transport security
  • service authorization
  • weighted traffic routing
  • canary releases
  • header-based routing
  • retries
  • timeouts
  • connection management
  • outlier detection
  • circuit breaking
  • metrics
  • distributed tracing

The most important production step after the technical installation is to move from a connectivity-first model to an identity-first security model: establish strict mTLS, identify legitimate service dependencies, and progressively express those dependencies as authorization policies.

Related Articles