Session Persistence

Session Persistence allows client requests to be consistently routed to the same backend service instance. This is useful in many scenarios, such as when an application needs to maintain state across multiple requests. In Envoy Gateway, session persistence can be enabled by configuring HTTPRoute.

Envoy Gateway supports following session persistence types:

  • Cookie-based Session Persistence: Session persistence is achieved based on specific cookie information in the request.
  • Header-based Session Persistence: Session persistence is achieved based on specific header information in the request.

Prerequisites

Install Envoy Gateway

Follow the steps below to install Envoy Gateway and the example manifest. Before proceeding, you should be able to query the example backend using HTTP.

Expand for instructions
  1. Install the Gateway API CRDs and Envoy Gateway using Helm:

    helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.3.2 -n envoy-gateway-system --create-namespace
    
  2. Install the GatewayClass, Gateway, HTTPRoute and example app:

    kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/v1.3.2/quickstart.yaml -n default
    
  3. Verify Connectivity:

    You can also test the same functionality by sending traffic to the External IP. To get the external IP of the Envoy service, run:

    export GATEWAY_HOST=$(kubectl get gateway/eg -o jsonpath='{.status.addresses[0].value}')
    

    Note: In certain environments, the load balancer may be exposed using a hostname, instead of an IP address. If so, replace ip in the above command with hostname.

    Curl the example app through Envoy proxy:

    curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get
    

    Get the name of the Envoy service created by the example Gateway:

    export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')
    

    Port forward to the Envoy service:

    kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80 &
    

    Curl the example app through Envoy proxy:

    curl --verbose --header "Host: www.example.com" http://localhost:8888/get
    

For better testing the session persistence, you can add more hosts in upstream cluster by increasing the replicas of one deployment:

kubectl patch deployment backend -n default -p '{"spec": {"replicas": 4}}'
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: cookie
spec:
  parentRefs:
    - name: eg
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: backend
          port: 3000
      sessionPersistence:
        sessionName: Session-A
        type: Cookie
        absoluteTimeout: 10s
        cookieConfig:
          lifetimeType: Permanent
EOF

Save and apply the following resource to your cluster:

---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: cookie
spec:
  parentRefs:
    - name: eg
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: backend
          port: 3000
      sessionPersistence:
        sessionName: Session-A
        type: Cookie
        absoluteTimeout: 10s
        cookieConfig:
          lifetimeType: Permanent

After updating the ConfigMap, you will need to wait the configuration kicks in.
You can force the configuration to be reloaded by restarting the envoy-gateway deployment.

kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system

Testing

Send a request to get a cookie and test:

COOKIE=$(curl --verbose http://localhost:8888/get 2>&1 | grep "set-cookie" | awk '{print $3}')
for i in `seq 5`; do
    curl -H "Cookie: $COOKIE" http://localhost:8888/get 2>/dev/null | grep pod
done

You can see all responses are from the same pod:

 "pod": "backend-765694d47f-lxwdf"
 "pod": "backend-765694d47f-lxwdf"
 "pod": "backend-765694d47f-lxwdf"
 "pod": "backend-765694d47f-lxwdf"
 "pod": "backend-765694d47f-lxwdf"

Wait for cookie expiration, test again:

sleep 10 && for i in `seq 5`; do
    curl -H "Cookie: $COOKIE" -H "Host: www.example.com" http://localhost:8888/get 2>/dev/null | grep pod
done
 "pod": "backend-765694d47f-kvwqb"
 "pod": "backend-765694d47f-lxwdf"
 "pod": "backend-765694d47f-kvwqb"
 "pod": "backend-765694d47f-2ff9s"
 "pod": "backend-765694d47f-lxwdf"

Due to cookie expiration, no session any more.

Header-based Session Persistence

cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: header
spec:
  parentRefs:
    - name: eg
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: backend
          port: 3000
      sessionPersistence:
        sessionName: Session-A
        type: Header
        absoluteTimeout: 10s
EOF

Save and apply the following resource to your cluster:

---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: header
spec:
  parentRefs:
    - name: eg
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: backend
          port: 3000
      sessionPersistence:
        sessionName: Session-A
        type: Header
        absoluteTimeout: 10s

Testing

Send a request to get a cookie and test:

HEADER=$(curl --verbose http://localhost:8888/get 2>&1 | grep "session-a" | awk '{print $3}')
for i in `seq 5`; do
    curl -H "Session-A: $HEADER" http://localhost:8888/get 2>/dev/null | grep pod
done

You can see all responses are from the same pod:

 "pod": "backend-765694d47f-gn7q2"
 "pod": "backend-765694d47f-gn7q2"
 "pod": "backend-765694d47f-gn7q2"
 "pod": "backend-765694d47f-gn7q2"
 "pod": "backend-765694d47f-gn7q2"

We remove the header and test again:

for i in `seq 5`; do
    curl http://localhost:8888/get 2>/dev/null | grep pod
done
 "pod": "backend-765694d47f-2ff9s"
 "pod": "backend-765694d47f-kvwqb"
 "pod": "backend-765694d47f-2ff9s"
 "pod": "backend-765694d47f-lxwdf"
 "pod": "backend-765694d47f-kvwqb"