WordPress on Kubernetes

The Definitive Guide for WordPress on k8s

Working with Persistent Volumes

You may have noticed that our current application lacks persistent storage. This means that any container crash or restart, pod restart, etc. will completely wipe our WordPress and MariaDB data, and have us start from scratch every time. Obviously this is undesirable.

To solve this problem we’ll need some persistent storage for both our WordPress application — for our themes and plugins, media uploads, etc.; as well as our database — for our post and page contents, configuration options and more.

Storage in Kubernetes is a large and complex topic, and we’ll touch on various interesting options along the way, but to keep things very simple for now, we’ll just use local volumes. A local persistent volume is essentially a directory (sometimes a disk or partition) on a Kubernetes node, that can be mounted into our containers. If you’ve worked with bind mounts in Docker before, this concept will be very familiar.

Node Affinity and Local Volumes

Local volumes exist on the physical disk attached to a specific node in our Kubernetes cluster. Only pods and containers running on that specific node will be able to use the volume. While this is not perfect (we’ll ultimately want our pods to be able to run on any node) it’s a good start for simple use cases, and we’ll explore more options in later sections.

In order to use local storage, we’ll first have to define a StorageClass in Kubernetes. This is a component that defines the type of storage, reclaim rules, provisioning and some other options and behaviours. We’ll manually provision our local storage on one of the nodes, so our StorageClass will be fairly simple. Let’s create a storage-class.yml file:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner

Nothing fancy here, but do note down the name we’ve assigned to our storage class, as we’ll have to use it later in our volumes and claims. Let’s now pick one of our nodes that will contain our local volumes for WordPress and MariaDB, and create the two directories there using SSH. We’re going to use the second node in our cluster with the hostname k1:

$ ssh k1
$ sudo mkdir -p /data/volumes/wordpress/www-data
$ sudo mkdir -p /data/volumes/wordpress/mariadb
$ exit

Next, let’s define these two volumes in a new `volumes.yml` file:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: www-data
spec:
  capacity:
    storage: 2Gi
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  local:
    path: /data/volumes/wordpress/www-data/
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k1

We define a persistent volume called www-data here, with 2GB of storage, a ReadWriteOnce access mode (you’ll learn about access modes in later sections), using our defined local-storage class. The local.path attribute is set to the location of our www-data directory on our node, and we also have a nodeAffinity section, forcing this volume to live on a Kubernetes node whose hostname matches k1 — that’s the one where we created our two directories.

Let’s create our second volume for mariadb in the same YAML file, separating the two Kubernetes objects with --- on its own line:

# first volume definition here ...
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mariadb
spec:
  capacity:
    storage: 2Gi
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  local:
    path: /data/volumes/wordpress/mariadb
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k1

Quite similar to our first volume. Let’s create our StorageClass and PersistentVolumes in Kubernetes with kubectl:

$ kubectl apply -f storage-class.yml -f volumes.yml
storageclass.storage.k8s.io/local-storage created
persistentvolume/www-data created
persistentvolume/mariadb created

Let’s look at claims next.

Persistent Volume Claims

Volumes alone are not enough to get things into our containers. We’ll need to define a couple of PersistentVolumeClaim objects, which are like requests for storage in Kubernetes. These can be satisfied with available volumes, like in our case, or in more common scenarios, a provisioner will dynamically create a new volume for such a request, we’ll look at some of these options in later sections.

Let’s create a volume-claims.yml file and define claims for both volumes:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: www-data
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  resources:
    requests:
      storage: 2Gi
  volumeName: www-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mariadb
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  resources:
    requests:
      storage: 2Gi
  volumeName: mariadb

We’re pretty much repeating the what we already defined in our volumes. We’re also using the volumeName attribute here, to make sure our claims are bound to the volumes with the correct names, otherwise we might end up in a situation, where our www-data claim is bound to the mariadb volume and vice-verca.

Add the claims to your Kubernetes cluster with kubectl:

$ kubectl apply -f volume-claims.yml
persistentvolumeclaim/www-data created
persistentvolumeclaim/mariadb created

You can then check the status of the claims, to make sure they’re bound to the correct volumes:

$ kubectl get pvc     
NAME       STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS    VOLUMEATTRIBUTESCLASS   AGE
mariadb    Bound    mariadb    2Gi        RWO            local-storage   <unset>                 30s
www-data   Bound    www-data   2Gi        RWO            local-storage   <unset>                 30s

Mounting Volumes into Containers

Now that our volumes and volume claims are ready, let’s update our pod.yml file to mount these volumes into the corresponding containers. First, let’s add a new volumes section on the same level as our containers section:

spec:
  containers:
  # ...

  volumes:
  - name: www-data
    persistentVolumeClaim:
      claimName: www-data
  - name: mariadb
    persistentVolumeClaim:
      claimName: mariadb

Here we tell Kubernetes which claims we would like to use for our Pod and ther names. Next, we’ll update our `wordpress` and `mariadb` containers specification to mount these volumes. The `wordpress` specification will now look like this:

spec:
  containers:
  - name: wordpress
    image: wordpress:6.4-apache
    ports:
    - containerPort: 80
    volumeMounts:
    - name: www-data
      mountPath: /var/www/html
    env:
    # environment vars ...

The new volumeMounts section tells Kubernetes to mount the www-data volume (which is liked to the www-data volume claim) into the /var/www/html directory inside our container. This is where Apache will be looking for our PHP files. Similarly, we’ll add the volume mount for MariaDB:

  - name: mariadb
    image: mariadb:10.11
    ports:
    - containerPort: 3306
    volumeMounts:
    - name: mariadb
      mountPath: /var/lib/mysql
    env:
    # environment vars ...

Here we mount the mariadb volume (linked to the mariadb PVC) to /var/lib/mysql, which is where MariaDB stores all its data.

The full pod.yml file at this stage can be found [here for reference]. The service.yml can be used as is from our previous section. Let’s create our pod and service in Kubernetes:

$ kubectl apply -f pod.yml -f service.yml
pod/wordpress created
service/wordpress created

Now let’s check the status of our new pod, to make sure that both containers are running.

$ kubectl get pods
NAME        READY   STATUS    RESTARTS   AGE
wordpress   2/2     Running   0          8s

Finally, point your web browser to the Kubernetes service and port by IP or hostname, run through the WordPress installer once more and feel free to add some plugins, themes and content to your WordPress website. All the data should now be persisted in our attached volumes. Let’s verify this by deleting our pod and creating it again:

$ kubectl delete pod wordpress
pod "wordpress" deleted

$ kubectl apply -f pod.yml
pod/wordpress created

Browse to the website once again, and verify that all the content is still there. We can also look at the contents of our two volumes via SSH directly on the Kubernetes node:

$ ssh k1
$ ls -l /data/volumes/wordpress/www-data/
total 248
-rw-r--r--  1 www-data www-data   405 Feb  6  2020 index.php
-rw-r--r--  1 www-data www-data 19915 Jun  9 23:12 license.txt
-rw-r--r--  1 www-data www-data  7401 Jun  9 23:12 readme.html
# ...

$ ls -l /data/volumes/wordpress/mariadb/
total 139460
-rw-rw---- 1 999 systemd-journal  16932864 Jun 10 10:06 aria_log.00000001
-rw-rw---- 1 999 systemd-journal        52 Jun 10 10:06 aria_log_control
-rw-rw---- 1 999 systemd-journal         9 Jun 10 10:07 ddl_recovery.log
-rw-rw---- 1 999 systemd-journal      1547 Jun 10 10:06 ib_buffer_pool
# ...

Recap & Cleanup

In our previous configuration all storage is ephemeral and lives only during the lifetime of the corresponding container. For the data to be persisted and available across restarts/recreates we’ll need to write it to persistent volumes in Kubernetes. In this section we’ve created and mounted two local storage persistent volumes to our WordPress and MariaDB containers.

This approach does have quite a few drawbacks. Since our volumes live on our k1 node in Kubernetes, we don’t have any options on where to schedule our Pods and containers, they have to live on the same node to make use of that storage. If the node happens to crash, we’re in trouble, losing both our running services, and potentially losing our WordPress and MariaDB data that’s on the disk attached to that node. We’ll address these and other problems in future sections where we expand on storage options in Kubernetes.

To delete your pod, service, volumes, claims and storage class, run:

$ kubectl delete -f pod.yml -f service.yml \
  -f volume-claims.yml -f volumes.yml \
  -f storage-class.yml

pod "wordpress" deleted
service "wordpress" deleted
persistentvolumeclaim "www-data" deleted
persistentvolumeclaim "mariadb" deleted
persistentvolume "www-data" deleted
persistentvolume "mariadb" deleted
storageclass.storage.k8s.io "local-storage" deleted

While the volumes are delete from the Kubernetes cluster, the volume data is still persisted on our disk on the k1 node. We’ll have to delete that manually via SSH, similar to how we provisioned it:

$ ssh k1
$ cd /data/volumes
$ sudo rm -rf www-data mariadb

All done here, see you in the next section!