In this tutorial you will learn how to deploy WordPress on Kubernetes with a multisite configuration. WordPress Multisite allows blog owners to host multiple sites from a single installation. This allows you manage all sites through a single pane of glass, minimizing your infrastructure spend and operational costs.
We’re going to use the official WordPress Docker image in this tutorial. WordPress offers images based on Apache and FPM, but we will be using the Apache image as a demonstration.
WordPress Docker images are configurable by setting environment variables. These environment variables must be set at container run-time to be available to WordPress.
The primary configuration of a WordPress installation:
An optional, but strongly recommended, configuration is the table prefix.
Multisite does not have a dedicated environment variable. Instead, we will need to enable it via the
WORDPRESS_CONFIG_EXTRA environment variable.
Storing Configs in Kubernetes
The database username and password should not be stored in clear text. Kubernetes has a secret resource that is specifically for storing sensitive information.
kubectl create secret generic wordpress-secret \ --from-literal=wordpress.db.user="wordpress_user" \ --from-literal=wordpress.db.password='my-super-secret-password'
Information that is not sensitive can be stored in a configMap. Let’s create a configMap to hold our database name, database prefix, and extra configurations.
Create a new manifest file named
wordpress-configmap.yaml and add the following contents:
apiVersion: v1 kind: ConfigMap metadata: name: wordpress-configmap data: wordpress.db.host: mysql.host:3306 wordpress.db.name: wordpress_site wordpress.db.prefix: blog_ wordpress.config.extra: | define('WP_ALLOW_MULTISITE', true);
To create the ConfigMap in your Kubernetes cluster use the
kubectl apply command.
kubectl apply -f wordpress-configmap.yaml
Containers are ephemeral by design. This is problematic for long lived applications like WordPress, who’s state changes frequently with uploaded content, updated themes, and updated plugins. To ensure our state persists beyond the life of the container running our site, we will need to create and mount a storage volume.
Create a PersistentVolumeClaim manifest file named
wordpress-pvc.yaml and add the following contents.
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: wp-pv-claim labels: app: wordpress spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi
Create the PersistentVolumeClaim in your cluster by applying the manifest file.
kubectl apply -f wordpress-pvc.yaml
Every Kubernetes Pod has an identity assigned to it. By default, if not service account is specified, a pod will be assigned the
default service account as its identity. As a best practice, every service you run in Kubernetes should be assigned it own service account.
Create a new service account for your WordPress pods using the following as an example.
apiVersion: v1 kind: ServiceAccount metadata: name: wordpress-multisite-sa automountServiceAccountToken: false
Every account in Kubernetes is assigned a unique token, which used by the API server to authenticate RESTful API requests. Tokens are stored as Secrets in the namespace a Service Account is created in, and it is mounted as file of every pod that uses the service account.
If a pod has no need to make API requests directly to Kubernetes, which is the case for nearly all workloads, the token should not be automatically mounted. In the example ServiceAccount manifest above, we’ve set
false, so that every deployment or pod that uses the
wordpress-multisite service account does not mount the token automatically.
WordPress Multisite Deployment Manifest
Finally, with our configuration information stored in
configMaps, and a
persistentVolumeClaim defined for our storage requirements, we can now create our
deployment manifest for WordPress.
A deployment manifest will ensure there are always at least 1 replicas of a pod running. If a pod fails a new one will be scheduled to replace it.
Create a new file named
wordpress-deployment.yaml and add the following contents.
apiVersion: apps/v1 kind: Deployment metadata: name: wordpress-multisite spec: replicas: 1 selector: matchLabels: app: wordpress template: metadata: labels: app: wordpress spec: serviceAccountName: wordpress-multisite-sa containers: - name: wordpress image: wordpress:5.6.0-php7.3-apache securityContext: capabilities: drop: ["ALL"] add: ["NET_BIND_SERVICE", "CHOWN"] ports: - containerPort: 80 env: - name: WORDPRESS_DB_HOST valueFrom: configMapKeyRef: name: wordpress-configmap key: wordpress.db.host - name: WORDPRESS_DB_USER valueFrom: secretKeyRef: name: wordpress-secret key: wordpress.db.user - name: WORDPRESS_DB_PASSWORD valueFrom: secretKeyRef: name: wordpress-secret key: wordpress.db.password - name: WORDPRESS_DB_NAME valueFrom: configMapKeyRef: name: wordpress-configmap key: wordpress.db.name - name: WORDPRESS_DB_PREFIX valueFrom: configMapKeyRef: name: wordpress-configmap key: wordpress.db.prefix - name: WORDPRESS_CONFIG_EXTRA valueFrom: configMapKeyRef: name: wordpress-configmap key: wordpress.config.extra volumes: - name: wordpress-persistent-storage persistentVolumeClaim: claimName: wp-pv-claim
While Docker and Containerd apply some sane defaults to protect containerized workloads, OCI containers are still largely vulnerabily and place your host and other workloads at risk. Even more can be and should be done to harden your processes to protect against malicious users.
Malicious users who find their way into your container via a flaw in your application will quickly realize they are in a container. Knowing this, they will attempt to escape the container in order to attack the host and all other containerized workloads. The key to container escapes is root privileges within the container and, unfortunately, the official WordPress images must run as root. While not good, it’s not completely terrible.
One way to protect a container and mitigate risks of escapes is to remove all Kernel capabilities granted to the parent processes' UID (0, root), except those absolutely required to run our app.
The deployment manifest above includes the following
securityContext for our Wordpress container. With it we instruct Kubernetes to drop all Linux kernel capabilities granted to the parent process (1) who runs as root (0), and then add only
NET_BIND_SERVICEcapability is required in order for the web server to bind to a privileged port (1-1024). In the case of a typical web application, that’s port 80.
CHOWNcapability is required as part of the Wordpress container’s startup process, where it applies new ownership to all Wordpress files.
securityContext: capabilities: drop: ["ALL"] add: ["NET_BIND_SERVICE", "CHOWN"]
By dropping all other Linux kernel capabilities we effectively neuter the root user’s privileges within the container. Since all processes are limited to the kernel capabilities granted to the parent process (PID 1), even new processes are limited to the capabilities of the parent. Therefore, a malicious user would not be able to create a new process with elevated kernel capabilities permissions within the container.
Create your deployment by applying the manifest above to your cluster.
kubectl apply -f wordpress-deployment.yaml