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