Istio: Routing

Modern applications are no longer built as single, monolithic systems—they’re composed of many small, interconnected services. Managing how these services communicate can quickly become complex, especially as systems scale. This is where Istio comes in.

Istio acts as a powerful service mesh that sits between your services and handles three critical concerns: traffic management, security, and observability.

With Istio, you gain fine-grained control over how traffic flows between services—enabling advanced deployment strategies like A/B testing and canary releases with ease. At the same time, it strengthens service-to-service security and provides deep visibility into your system through metrics, logs, and tracing.

For this post lets do a practical example in using istio.

Repo: mcbtaguiad/istio-demo

Table of Contents

Install

istioctl

Check this link for different option to install istio. For this example we’ll be using istioctl.

Download the istio release. This will add istio binary to $HOME/istio-<version>/bin.

1curl -L https://istio.io/downloadIstio | sh -
2export PATH="$PATH:$HOME/istio-<version>/bin"

Profile

An Istio profile is essentially a predefined configuration that determines which components are installed and how they’re set up.

We need to set the profile when installing istio, the default profile is recommended for production deployment. Check this link if you are interested in reading more about istio profile.

Platform

The platform setting tells Istio what kind of Kubernetes environment it is running on.

Install

1istioctl install --set profile=default --set values.global.platform=k3s

Notice if you visit the platform link kubeadm is missing. Since this is the standard for kubernetes you don’t need to set platform.

1istioctl install --set profile=default

Verify contents.

 1kubectl get all -n istio-system
 2NAME                                       READY   STATUS    RESTARTS   AGE
 3pod/istio-ingressgateway-b7dbbb799-j9rsg   1/1     Running   0          38s
 4pod/istiod-78bf998bbf-s4dnw                1/1     Running   0          50s
 5
 6NAME                                  TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)                                      AGE
 7service/istio-ingressgateway          LoadBalancer   10.43.114.134   192.168.254.223   15021:30646/TCP,80:31914/TCP,443:30599/TCP   38s
 8service/istiod                        ClusterIP      10.43.82.154    <none>            15010/TCP,15012/TCP,443/TCP,15014/TCP        50s
 9service/istiod-revision-tag-default   ClusterIP      10.43.162.58    <none>            15010/TCP,15012/TCP,443/TCP,15014/TCP        26s
10
11NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
12deployment.apps/istio-ingressgateway   1/1     1            1           38s
13deployment.apps/istiod                 1/1     1            1           51s
14
15NAME                                             DESIRED   CURRENT   READY   AGE
16replicaset.apps/istio-ingressgateway-b7dbbb799   1         1         1       38s
17replicaset.apps/istiod-78bf998bbf                1         1         1       51s
18
19NAME                                                       REFERENCE                         TARGETS       MINPODS   MAXPODS   REPLICAS   AGE
20horizontalpodautoscaler.autoscaling/istio-ingressgateway   Deployment/istio-ingressgateway   cpu: 3%/80%   1         5         1          38s
21horizontalpodautoscaler.autoscaling/istiod                 Deployment/istiod                 cpu: 0%/80%   1         5         1          51s

Istio Environment

Istio Sidecar

In order to take advantage of all of Istio’s features, pods in the mesh must be running an Istio sidecar proxy.

Before we deploy an application let’s enable istio-injection=enabled to the namespace demo.

1kubectl label namespace demo istio-injection=enabled

Restart pod if pod is already present.

Gateway

There is two approach to expose and route traffic using istio: the traditional Istio API and the newer Kubernetes Gateway API.

Istio API (Traditional Approach)

This relies on a shared ingress gateway deployed in the istio-system namespace. The LoadBalancer is reused across applications in the cluster.

Uses Istio-specific resources like:

  • Gateway
  • VirtualService
1kubectl get pods -n istio-system
2NAME                                   READY   STATUS    RESTARTS   AGE
3istio-ingressgateway-b7dbbb799-j9rsg   1/1     Running   0          6h27m
4istiod-78bf998bbf-s4dnw                1/1     Running   0          6h27m

Gateway API (Kubernetes-Native Approach)

In this approach a LoadBalancer is always deployed on all injected namespace. This is more declarative, Kubernetive-native and future-proof, so this will be the approach used in this example/demo.

Uses Istio-specific resources like:

  • Gateway
  • HTTPRoute

Install Gateway-api.

1kubectl get crd gateways.gateway.networking.k8s.io &> /dev/null || \
2{ kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.4.0" | kubectl apply -f -; }
3
4customresourcedefinition.apiextensions.k8s.io/backendtlspolicies.gateway.networking.k8s.io created
5customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io created
6customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io created
7customresourcedefinition.apiextensions.k8s.io/grpcroutes.gateway.networking.k8s.io created
8customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io created
9customresourcedefinition.apiextensions.k8s.io/referencegrants.gateway.networking.k8s.io created

Deploy

Create gateway in demo namespace.

Clone this repo mcbtaguiad/istio-demo.

1git clone https://github.com/mcbtaguiad/istio-demo.git
2cd istio-demo
3kubectl create -f routing/gateway-api/gateway.yaml -n demo

Verify.

 1kubectl get all -n demo 
 2NAME                                        READY   STATUS    RESTARTS   AGE
 3pod/demo-app-gateway-istio-dd965bf4-htppk   1/1     Running   0          45s
 4
 5NAME                             TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)                        AGE
 6service/demo-app-gateway-istio   LoadBalancer   10.43.150.164   192.168.254.224   15021:32268/TCP,80:30324/TCP   45s
 7
 8NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE
 9deployment.apps/demo-app-gateway-istio   1/1     1            1           45s
10
11NAME                                              DESIRED   CURRENT   READY   AGE
12replicaset.apps/demo-app-gateway-istio-dd965bf4   1         1         1       45s

Application

Diagram

Honestly use the book example of istio.

This is a slight derivation of the book example, just need an excuse to uplift my boredom so I created this app-also to fully understand istio (smirk). Application

The diagram will make sense as we progress. Visit this diagram as we explore istio application.

Deploy Application

1kubectl create -k kube/demo/environments/demo

Verify and take notes of the content here specially the services.

 1kubectl get all -n demo
 2NAME                                         READY   STATUS    RESTARTS   AGE
 3pod/backend-v1-579f8fdc8b-mjm79              2/2     Running   0          4h12m
 4pod/backend-v2-7c6675d6b8-gbkkx              2/2     Running   0          4h12m
 5pod/demo-app-gateway-istio-6c4c964fb-svp6s   1/1     Running   0          9h
 6pod/frontend-6fc6b84f46-hc9g4                2/2     Running   0          3h54m
 7pod/monitor-v1-97778d5-ft2lq                 2/2     Running   0          4h12m
 8pod/monitor-v2-6f789fd44c-6hngx              2/2     Running   0          4h12m
 9pod/redis-79c7d6dd4-w8trn                    2/2     Running   0          4h12m
10
11NAME                             TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)                        AGE
12service/backend                  ClusterIP      10.43.90.82     <none>            3000/TCP                       4h12m
13service/backend-v1               ClusterIP      10.43.240.7     <none>            3000/TCP                       4h12m
14service/backend-v2               ClusterIP      10.43.146.111   <none>            3000/TCP                       4h12m
15service/demo-app-gateway-istio   LoadBalancer   10.43.7.6       192.168.254.221   15021:30942/TCP,80:32687/TCP   9h
16service/frontend                 ClusterIP      10.43.130.214   <none>            80/TCP                         4h12m
17service/monitor                  ClusterIP      10.43.129.195   <none>            8000/TCP                       4h12m
18service/monitor-v1               ClusterIP      10.43.108.137   <none>            8000/TCP                       4h12m
19service/monitor-v2               ClusterIP      10.43.190.193   <none>            8000/TCP                       4h12m
20service/redis                    ClusterIP      10.43.125.209   <none>            6379/TCP                       4h12m
21
22NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE
23deployment.apps/backend-v1               1/1     1            1           4h12m
24deployment.apps/backend-v2               1/1     1            1           4h12m
25deployment.apps/demo-app-gateway-istio   1/1     1            1           9h
26deployment.apps/frontend                 1/1     1            1           4h12m
27deployment.apps/monitor-v1               1/1     1            1           4h12m
28deployment.apps/monitor-v2               1/1     1            1           4h12m
29deployment.apps/redis                    1/1     1            1           4h12m
30
31NAME                                               DESIRED   CURRENT   READY   AGE
32replicaset.apps/backend-v1-579f8fdc8b              1         1         1       4h12m
33replicaset.apps/backend-v2-7c6675d6b8              1         1         1       4h12m
34replicaset.apps/demo-app-gateway-istio-6c4c964fb   1         1         1       9h
35replicaset.apps/frontend-6fc6b84f46                1         1         1       3h54m
36replicaset.apps/frontend-c7dbbbf6c                 0         0         0       4h12m
37replicaset.apps/monitor-v1-97778d5                 1         1         1       4h12m
38replicaset.apps/monitor-v2-6f789fd44c              1         1         1       4h12m
39replicaset.apps/redis-79c7d6dd4                    1         1         1       4h12m

Routing

Gateway

The key here is we want to control the traffic going to the application using istio, if you look at the diagram we have version 1 and version 2 on backend and monitor service. That’s were istio routing comes along, let’s demostrate that here.

Througout the routing example, make sure to always reference the gateway we created ealier.

1spec:
2  parentRefs:
3    - name: demo-app-gateway

Default Routing / Round Robin

In here we want to balance the route to both version 1 and version 2 on frontend and backend.

Create the HTTPRoute.

route-round-robin.yaml

 1apiVersion: gateway.networking.k8s.io/v1
 2kind: HTTPRoute
 3metadata:
 4  name: demo-app-rr
 5spec:
 6  parentRefs:
 7    - name: demo-app-gateway
 8  rules:
 9    # Backend API 
10    - matches:
11        - path:
12            type: RegularExpression
13            value: /api/.*
14      backendRefs:
15        - name: backend
16          port: 3000
17
18    # Monitor /status
19    - matches:
20        - path:
21            type: PathPrefix
22            value: /status
23      backendRefs:
24        - name: monitor
25          port: 8000
26
27    # Frontend /app 
28    - matches:
29        - path:
30            type: PathPrefix
31            value: /app
32      backendRefs:
33        - name: frontend
34          port: 80
1kubectl create -f routing/gateway-api/route-round-robin.yaml -n demo

This will use as the services bellow as backendRefs.

1backend                  ClusterIP      10.43.90.82     <none>            3000/TCP                       4h14m
2frontend                 ClusterIP      10.43.130.214   <none>            80/TCP                         4h14m
3monitor                  ClusterIP      10.43.129.195   <none>            8000/TCP                       4h14m

Analyze Istio configuration for issues. This will report any configuration problems.

1istioctl analyze -n demo

Verify using the gateway endpoint - check the IP on your gateway-api service.

1# app landing page
2http://192.168.254.221/app
3
4# monitor
5http://192.168.254.221/status

Cycle through /login, /register and /backend, you’ll notice the version is changing randomly.

If you are curious about Path Matching, look at the table below.

Match Type Description Example
Exact Matches the path exactly /login matches only /login
Prefix Matches the path and any sub-paths /app matches /app, /app/page1, /app/api
RegularExpression Matches paths using a regex /app/.* matches /app/page1 and /app/page2

Route to Specific Version

In here we route backend to version 2 and monitor to versin 1 exclusively.

route-to-specific-version.yaml

 1apiVersion: gateway.networking.k8s.io/v1
 2kind: HTTPRoute
 3metadata:
 4  name: demo-app-spec-ver
 5spec:
 6  parentRefs:
 7    - name: demo-app-gateway
 8  rules:
 9    #  Backend API 
10    - matches:
11        - path:
12            type: RegularExpression
13            value: /api/.*
14      backendRefs:
15        - name: backend-v2
16          port: 3000
17
18    #  Monitor /status
19    - matches:
20        - path:
21            type: PathPrefix
22            value: /status
23      backendRefs:
24        - name: monitor-v1
25          port: 8000
26
27    # Frontend /app 
28    - matches:
29        - path:
30            type: PathPrefix
31            value: /app
32      backendRefs:
33        - name: frontend
34          port: 80
1kubectl create -f routing/gateway-api/route-to-specific-version.yaml -n demo

Route Specific User

In this setup, the Go application authenticates users using JWT (JSON Web Tokens). After a successful login, user-specific information (e.g., username or role) is extracted from the JWT and injected into a custom HTTP header, such as:

1x-user: jonathan

Of course this will depend on how you create your application.

User jonathan will be routed to version 1 and all other users to version 2.

To make it easier to debug or demonstrate lets add a ResponseHeaderModifier.

1      filters:
2        - type: ResponseHeaderModifier
3          responseHeaderModifier:
4            add:
5              - name: x-version
6                value: "1.0.0"

route-to-user.yaml

 1apiVersion: gateway.networking.k8s.io/v1
 2kind: HTTPRoute
 3metadata:
 4  name: demo-app-route-user-specific
 5spec:
 6  parentRefs:
 7    - name: demo-app-gateway
 8  rules:
 9    #  Backend API 
10    - matches:
11      - headers:
12        - name: x-user
13          type: Exact
14          value: "jonathan"
15        path:
16          type: RegularExpression
17          value: /api/.*
18      filters:
19        - type: ResponseHeaderModifier
20          responseHeaderModifier:
21            add:
22              - name: x-version
23                value: "1.0.0"
24      backendRefs:
25        - name: backend-v1
26          port: 3000
27
28    - matches:
29        - path:
30            type: RegularExpression
31            value: /api/.*
32      filters:
33        - type: ResponseHeaderModifier
34          responseHeaderModifier:
35            add:
36              - name: x-version
37                value: "2.0.0"
38      backendRefs:
39        - name: backend-v2
40          port: 3000
41
42
43    #  Monitor /status
44    - matches:
45        - path:
46            type: PathPrefix
47            value: /status
48      backendRefs:
49        - name: monitor
50          port: 8000
51
52    #  SPA catch-all
53    - matches:
54      
55        - path:
56            type: PathPrefix
57            value: /app
58      backendRefs:
59        - name: frontend
60          port: 80
1kubectl create -f routing/gateway-api/route-to-user.yaml -n demo

Get user token. You’ll notice that request is always send through istio-envoy. User jonathan is routed to version 1.

 1curl -i -X POST http://192.168.254.221/api/login  -H "Content-Type: application/json"   -d '{"username":"jonathan","password":"123"}'
 2HTTP/1.1 200 OK
 3vary: Origin
 4date: Sat, 28 Mar 2026 10:20:22 GMT
 5content-length: 170
 6content-type: text/plain; charset=utf-8
 7x-envoy-upstream-service-time: 85
 8server: istio-envoy
 9x-version: 2.0.0
10
11curl -i http://192.168.254.221/api/users \                                                                                          
12  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzQ3Nzk2MjIsImlzcyI6ImRlbW8tYXBwIiwidXNlcm5hbWUiOiJqb25hdGhhbiJ9.QRpu3WT3yHuMz5RwZmXsKNTfD2VtJf21XlZcD6Jg3ak" \
13  -H "x-user: jonathan"
14HTTP/1.1 200 OK
15vary: Origin
16date: Sat, 28 Mar 2026 10:20:57 GMT
17content-length: 477
18content-type: text/plain; charset=utf-8
19x-envoy-upstream-service-time: 9
20server: istio-envoy
21x-version: 1.0.0
22
23{"total":12,"users":[{"username":"3213","group":"alpha"},{"username":"user001","group":"alpha"},{"username":"user002","group":"beta"},{"username":"test","group":"alpha"},{"username":"admin","group":"alpha"},{"username":"user004","group":"beta"}

Inspect x-version: 1.0.0.

Test if other users are routed to version 2

 1curl -i -X POST http://192.168.254.221/api/login  -H "Content-Type: application/json"   -d '{"username":"user001","password":"123"}'
 2HTTP/1.1 200 OK
 3vary: Origin
 4date: Sat, 28 Mar 2026 10:29:55 GMT
 5content-length: 169
 6content-type: text/plain; charset=utf-8
 7x-envoy-upstream-service-time: 98
 8server: istio-envoy
 9x-version: 2.0.0
10
11{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzQ3ODAxOTUsImlzcyI6ImRlbW8tYXBwIiwidXNlcm5hbWUiOiJ1c2VyMDAxIn0.OhnP-b6wFqJI1eUf0dyMhISoby49iPgY_Rv0Sy-XWTY"}
12
13curl -i http://192.168.254.221/api/users \
14  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzQ3ODAxOTUsImlzcyI6ImRlbW8tYXBwIiwidXNlcm5hbWUiOiJ1c2VyMDAxIn0.OhnP-b6wFqJI1eUf0dyMhISoby49iPgY_Rv0Sy-XWTY" \  
15  -H "x-user: user001"
16HTTP/1.1 200 OK
17vary: Origin
18date: Sat, 28 Mar 2026 10:30:43 GMT
19content-length: 477
20content-type: text/plain; charset=utf-8
21x-envoy-upstream-service-time: 11
22server: istio-envoy
23x-version: 2.0.0
24
25{"total":12,"users":[{"username":"3213","group":"alpha"},{"username":"user001","group":"alpha"},{"username":"user002","group":"beta"},{"username":"test","group":"alpha"},{"username":"admin","group":"alpha"},{"username":"user004","group":"beta"},{"username":"jonathan","group":"alpha"},{"username":"user005","group":"beta"},{"username":"123123","group":"beta"},{"username":"test001","group":"alpha"},{"username":"user003","group":"beta"},{"username":"jonthan","group":"beta"}]}

This verifies that user jonathan is routed to version 1 and other users are routed to version 2.

Deployment/Testing Strategies

With this we can build testing/deployment strategies like A/B, Canary and Blue/Green. I will discuss this on a separate post.