Istio routing based on headers without header propagation

Ric Hincapie
5 min readFeb 19, 2024

--

Istio uses Envoy proxy as a Pod sidecar to which the application delegates networking responsibilities like the inbound and outbound traffic, but there’s one responsibility that still holds the App container, which is the headers propagation.

The Envoy proxy cannot correlate the packets that it sends to the App to those that the App is responding to, so the headers cannot be automatically propagated by Istio.

Sidecar can’t correlate requests with responses if the app container does not forward back headers

In most cases, a headers-based routing would need the App developers to implement a library. For example, in Istio’s flagship Bookinfo app, the productpage microservice implements it like this. This drive us to a question:

How can a Platform Admin have headers-based routing without modifying the application’s internals?

A swim-lane approach

Using Bookinfo app, we’re segmenting different request paths based on a x-version header like:

The implementation will randomly select backends paths if no x-version header is present.

Deploy workloads

We will use Istio’s Bookinfo example with some minor changes regarding versioning apps to show an implementation.

First of all, create 3 different productpage deployments, only changing the version labels.

apiVersion: apps/v1
kind: Deployment
metadata:
name: productpage-v{1,2,3}
labels:
app: productpage
version: v{1,2,3}
spec:
replicas: 1
selector:
matchLabels:
app: productpage
version: v{1,2,3}
template:
metadata:
labels:
app: productpage
version: v{1,2,3}
...

And one service for them all:

apiVersion: v1
kind: Service
metadata:
name: productpage
labels:
app: productpage
service: productpage
spec:
ports:
- port: 9080
name: http
selector:
app: productpage

Then, create 3 deployments for reviews app:

apiVersion: apps/v1
kind: Deployment
metadata:
name: reviews-v{1,2,3}
labels:
app: reviews
version: v{1,2,3}
spec:
replicas: 1
selector:
matchLabels:
app: reviews
version: v{1,2,3}
template:
metadata:
labels:
app: reviews
version: v{1,2,3}
...

Ratings and details apps just keep the same as in the original example.

Deploy Istio configs

Here’s where Istio’s routing capabilities come into play. A DestinationRule subsetsfor each productpage version needs to be defined:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: productpage
spec:
host: productpage
trafficPolicy:
loadBalancer:
simple: RANDOM
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
- name: v3
labels:
version: v3

And a couple of VirtualService that implement the first half of the swim-lane headers logic. The following takes charge of the prefix matches and uses the delegate functionality to use a second VirtualService, so configs are atomic and declaring mesh gateway selector is avoided (see quote below):

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: bookinfo
spec:
hosts:
- "*"
gateways:
- bookinfo-gateway
http:
- match:
- uri:
exact: /productpage
- uri:
prefix: /static
- uri:
exact: /login
- uri:
exact: /logout
- uri:
prefix: /api/v1/products
delegate:
name: productpage-route

Now the delegated productpage-route:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: productpage-route
spec:
http:
- name: "productpage-v1-route"
match:
- headers:
x-version:
exact: v1
route:
- destination:
host: productpage
subset: v1
- name: "productpage-v2-route"
match:
- headers:
x-version:
exact: v2
route:
- destination:
host: productpage
subset: v2
- name: "productpage-v3-route"
match:
- headers:
x-version:
exact: v3
route:
- destination:
host: productpage
subset: v3
- name: "productpage-default-route"
match:
- withoutHeaders:
x-version: {}
route:
- destination:
host: productpage

See how the last match called productpage-default-route takes care of request with no x-version header. In this case, a random response from any of the lanes will be given. Note the random case is not in the diagram at the beginning of this blog post.

Then, at the Reviews level, craft the second half of the swim-lane using the sourceLabels config at the httpMatchRequest:

One or more labels that constrain the applicability of a rule to source (client) workloads with the given labels. If the VirtualService has a list of gateways specified in the top-level gateways field, it must include the reserved gateway mesh for this field to be applicable.

Source: https://istio.io/latest/docs/reference/config/networking/virtual-service/#HTTPMatchRequest

This is the VirtualService using the sourceLabels feature:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews-route
spec:
hosts:
- reviews
http:
- name: "reviews-v1-route"
match:
- sourceLabels:
version: v1
route:
- destination:
host: reviews
subset: v1
- name: "reviews-v2-route"
match:
- sourceLabels:
version: v2
route:
- destination:
host: reviews
subset: v2
- name: "reviews-v3-route"
match:
- sourceLabels:
version: v3
route:
- destination:
host: reviews
subset: v3

Testing header routing without header propagation

First, start with the no header scenario, where you get responses from all lanes:

  —————»  ns:bookinfo ❯ for i in {1..5}; do curl -s localhost:8080/productpage | grep -A1 "Reviews served by"; done
<dt>Reviews served by:</dt>
<u>reviews-v2-955b74755-t4jkb</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v2-955b74755-t4jkb</u>

Then check if the x-version: v1 header has any effect. You can see all the calls productpage-v1 workload make are being served exclusively by reviews-v1 .

  —————»  ns:bookinfo ❯ for i in {1..10}; \
do curl -s localhost:8080/productpage -H "x-version: v1" \
| grep -A1 "Reviews served by"; done
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>
<dt>Reviews served by:</dt>
<u>reviews-v1-5cf854487-hjtrg</u>

And finish testing with v3 header value:

  —————»  ns:bookinfo ❯ for i in {1..10}; \
do curl -s localhost:8080/productpage -H "x-version: v3" \
| grep -A1 "Reviews served by"; done
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>
<dt>Reviews served by:</dt>
<u>reviews-v3-797fc48bc9-wsg26</u>

In this article you could see how using match on headers, subsets and sourceLabels in Istio it is possible to route based on headers with no header propagation. You could also see the usage of delegate functionality as well as withoutHeaders matching.

--

--

Ric Hincapie
Ric Hincapie

No responses yet