Kubernetes Cluster in a Weekend - Part 2#

Deploying an App - StatefulSets#

I decided to deploy a wiki application to the Kubernetes cluster to have a place to store documentation for research, new projects, and existing system architectures. I foundĀ BookStatck to be simple but functional app to get running in the K3s cluster.

BookStack will run in two pods each with dedicated storage. A Pod consisted of one or more containers and is the smallest unit of work in Kubernetes. While use cases do exist for multiple Containers in a Pod, normally, only one is assigned. I created a Pod for BookStacks backend SQL database as well as a Pod for the application itself. These Pods are managed as a StatefulSet allowing for stable identifiers and persistent storage, provisioned with Longhorn..

Starting with MariaDB.

---
apiVersion: v1
kind: Secret
metadata:
  name: bookstack-db-password
type: Opaque
data:
  password: OG5CI1c6Pywocn5JWmVtPThLSV5OTT9EM11iP0QvKE4K

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: mariadb
    partOf: bookstack
  name: bookstack-db
  namespace: apps
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mariadb
      partOf: bookstack
  template:
    metadata:
      labels:
        app: mariadb
        partOf: bookstack
    spec:
      containers:
      - env:
          - name: MYSQL_DATABASE
            value: bookstackapp
          - name: MYSQL_PASSWORD
            valueFrom:
              secretKeyRef:
                name: bookstack-db-password
                key: password
          - name: MYSQL_USER
            value: bookstack
          - name: PGID
            value: "1000"
          - name: PUID
            value: "1000"
          - name: TZ
            value: UTC
        image: lscr.io/linuxserver/mariadb
        name: bookstack-db
        volumeMounts:
        - mountPath: /config
          name: storage
  volumeClaimTemplates:
  - metadata:
      name: storage
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
      storageClassName: longhorn

---
apiVersion: v1
kind: Service
metadata:
  labels:
    partOf: bookstack
  name: bookstack-db-svc
  namespace: apps
spec:
  clusterIP: None  # Headless service
  ports:
    - name: mysql
      port: 3306
      protocol: TCP
      targetPort: 3306
  selector:
    app: mariadb
    partOf: bookstack

MariaDB is used with a volumeClaimTemplate to allocated disk space for persistent SQL database storage.

The configuration accessModes: ReadWriteOnce instructs the cluster that only one worker node is allowed to mount and write to this volume.

Let's break down the StatefulSet manifest starting with replicas: 1 and continuing with selector: ... . This configuration tells K3s that only one Pod named with a label of app: bookstack. If the value of replicas were set to 3, two more Bookstack Pods would be created based on the information in the section template:.

Container applications can be configured with environment variables. Here, I use env: to define the SQL database username and password (stored as a K8 Secret) Bookstack will use, as well as other app specific configurations. An image: lscr.io/linuxserver/mariadb is also configured to inform K3s what container to fetch.

Skipping over volumeMounts: ... briefly to describe volumeClaimTemplates: ... section. StatefulSets can leverage a data volume for persistent storage giving the workload a dedicated location to save application data, while still allowing create, updated, delete action on the Pods. New Pods with the same name automatically have access to the data volume.

Configured volumeMounts: ... add the data volume inside the Pod at the mountPath.

---

apiVersion: v1
kind: Secret
metadata:
  name: bookstack
type: Opaque
data:
  # openssl rand -base64 24
  # base64:0FWfiMKrr5XDokyFe6fpTd9THICpkzQcWZkS/vb3hbw=
  appkey: YmFzZTY0OjBGV2ZpTUtycjVYRG9reUZlNmZwVGQ5VEhJQ3BrelFjV1prUy92YjNoYnc9

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: bookstack
    partOf: bookstack
  name: bookstack-app
  namespace: apps
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bookstack
      partOf: bookstack
  template:
    metadata:
      labels:
        app: bookstack
        partOf: bookstack
    spec:
      containers:
      - env:
          - name: APP_URL
            value: http://10.33.0.152 # Replace with FQDN
          - name: DB_DATABASE
            value: bookstackapp
          - name: DB_HOST
            value: bookstack-db-svc
          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: bookstack-db-password
                key: password
          - name: DB_USERNAME
            value: bookstack
          - name: APP_KEY
            valueFrom:
              secretKeyRef:
                name: bookstack
                key: appkey
          - name: PGID
            value: "1000"
          - name: PUID
            value: "1000"
        image: lscr.io/linuxserver/bookstack
        name: bookstack-app
        ports:
          - containerPort: 80
        volumeMounts:
          - mountPath: /config
            name: storage

  volumeClaimTemplates:
  - metadata:
      name: storage
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
      storageClassName: longhorn

control-01:~/apps/bookstack$ kubectl apply -f bookstack-db-statefulset.yaml -f bookstack-statefulset.yaml

There should now be two Pod running for Bookstack, bookstack-app and bookstack-db. We can check with kubectl get pod -l partOf=bookstack -A

Both Pods can communicate with each other, but neither is accessible outside the K3s cluster. This is where Kubernetes (K3s in this case) can be though of as a software defined datacenter. Everything is racked, stacked and network patched inside the cluster, but traffic has no path in or out.

Accessing BookStack - Services#

Services provide a mechanism to expose application outside the cluster. MetalLB, a component described in a previous log, operates by allocating an IP address from the accessible pool to any Services with type: LoadBalancer. Services expose a port/protoco combination, in this case TCP 6875, directing traffic back to port 80 in the Pod (targetPort). ``

---
apiVersion: v1
kind: Service
metadata:
  annotations:
  labels:
    partOf: bookstack
  name: bookstack-svc
  namespace: apps
spec:
  type: LoadBalancer
  ports:
    - name: "tcp-6875"
      port: 6875
      protocol: TCP
      targetPort: 80
  selector:
    app: bookstack

A IP address for the service will be allocated from MetaLB's address pool. We can see, 10.33.0.152, after deploying the manifest then checking the available services. Bookstack is available on that IP and port (6875) through your favorite browser.

control-01:~/apps/bookstack$ kubectl get services -l partOf=bookstack -A

NAME               TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        
bookstack-db-svc   ClusterIP      None           <none>        3306/TCP         
bookstack-svc      LoadBalancer   10.43.16.134   10.33.0.152   6875:31128/TCP   

Architecture Diagram#

k3-statefulset

< back