Sticky Sessions in Kubernetes

In the migration journey to the cloud, not all apps are stateless immediately. Sometimes, we still need the session affinity or sticky session for the request to come to the same pod replica that was responding to the request before.

Nginx ingress controller

The kubernetes ingress controllers, such as Nginx ingress controller already has these requirement considered and implemented. The ingress controller replies the response with a Set-Cookie header to the first request. The value of the cookie will map to a specific pod replica. When the subsequent request come back again, the client browser will attach the cookie and the ingress controller is therefore able to route the traffic to the same pod replica.

The configuration is normally achieved with annotations. See the following example from https://kubernetes.github.io/ingress-nginx/examples/affinity/cookie/ingress.yaml.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx-test
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "route"
nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"

spec:
rules:
- host: stickyingress.example.com
http:
paths:
- backend:
serviceName: http-svc
servicePort: 80
path: /

By setting the annotations in the ingress object, we can make sure the subsequent request will still be served by the same pod.

Traefik ingress controller on K3s

K3s comes by default with the Traefik controller. I will demonstrate the session affinity with it.

Create a sample app

Let's have a toy golang app, whose HTTP handler will just print out the hostname.

package main
import (
"fmt"
"net/http"
"os"
)
func greet(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "You are served by host:%s.", os.Getenv("HOSTNAME"))
}
func main() {
http.HandleFunc("/", greet)
http.ListenAndServe(":8080", nil)
}

Compile it, build a Docker image and push into docker hub. Attached is the Dockerfile

FROM alpine
RUN mkdir -p /myapp
ADD myapp /myapp
RUN chmod a+rx /myapp/myapp
CMD ["/myapp/myapp"]

Deploy into K3s

Nothing special, just export the KUBECONFIG, you have the familiar kubectl the same as K8s.

export KUBECONFIG=~/k3s/k3s.yaml

Apply the following sample Kubernetes object files, including ingress, service, and deployment.

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: traefik
labels:
app: session-affinity
name: session-affinity
spec:
rules:
- host: sess.192.168.64.5.nip.io
http:
paths:
- path: /
backend:
serviceName: session-affinity
servicePort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: session-affinity
labels:
app: session-affinity
annotations:
traefik.ingress.kubernetes.io/affinity: "true"
traefik.ingress.kubernetes.io/session-cookie-name: "sticky"

spec:
type: NodePort
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: session-affinity-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: session-affinity-demo
labels:
app: session-affinity-demo
spec:
replicas: 3
selector:
matchLabels:
app: session-affinity-demo
template:
metadata:
labels:
app: session-affinity-demo
spec:
containers:
- name: session-affinity-demo
image: zhiminwen/session-sticky:v1.0

In the ingress object, we instruct it in the annotations to use the Traefik ingress controller. Note the sticky session in Traefik is defined in the Service object with the annotation, which is different comparing with the Nginx ingress controller. Instead of a random cookie name, we define it as “sticky”.

We have 3 replicas running.

Testing

Launch the browser, go to the expected Ingress host, sess.192.168.64.5.nip.io

You can see the greeting message of the hostname. Note upon the very first response, the Set-Cookie is set in the response header.

Refresh the browser, you will see the request is served by the same pod. This was because of the Cookie which was set by the browser.