How to deploy WordPress on Kubernetes using Gitlab
Mar 14, 2023
8 min read
281 views
Share

This tutorial will guide you in deploying a WP application in Kubernetes using the GitLab repository and a remote server with a MySQL database. Also added a manifest for Kubernetes cron job for data backup to AWS S3 bucket.

Create a repository on GitLab

Put the source of code of WP site or add folders with content to the repository from the archive: wp-content/themes, wp-content/plugins, wp-content/uploads.

Configure the CI/CD variables for the project.

You must have a Kubernetes cluster and the kubectl command-line tool that must be configured to communicate with your cluster.

 

Variables :

– WORDPRESS_DB_HOST

– WORDPRESS_DB_USER

– WORDPRESS_DB_PASSWORD

– WORDPRESS_DB_NAME

– WORDPRESS_TABLE_PREFIX

Variables :

– DB_HOST

– DB_USER

– DB_PASSWORD

– DB_NAME

Ceate Dockerfile

FROM wordpress:6.1.1-php8.0-apache

ADD wp-content/themes /usr/src/wordpress/wp-content/themes
ADD wp-content/uploads /usr/src/wordpress/wp-content/uploads
ADD wp-content/plugins /usr/src/wordpress/wp-content/plugins

ADD ./backup.sh /opt/scripts/

EXPOSE 80

At the first download, the files copied to the /usr/src/wordpress/ folder will be automatically located in the /var/www/html/ folder and belong to the www-data user.

The owner of the /var/www/html/ folder must be www-data only, otherwise, it will not be possible to download and update themes, plugins, and files through the WP editor.

But since we will be using persistentVolumeClaim we will need to copy all the files from folder wp-content/themes, wp-content/uploads, wp-content/plugins again after creating Persistent Volume Claim.

Create deployment.yml manifest and other files

The following manifest describes a WP deployment. PersistentVolumeClaim with mounts  /var/www/html/wp-content/uploads, /var/www/html/wp-content/plugins, /var/www/html/wp-content/themes are necessary so that folders and files that were added not to the repository do not disappear when the pod is recreating.

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ${PROJECT_NAME}
  ...#Add lines depending on your Kubernetes settings
    spec:
      containers:
      - name: ${PROJECT_NAME}
        image: ${REPOSITORY_IMAGE}
        ports:
        - containerPort: ${CONTAINER_PORT}
        ...
        # Make sure, that appropriate subpaths exists in shared volume
        volumeMounts:
          - name: ${PROJECT_NAME}
            mountPath: /var/www/html/wp-content/uploads
            subPath: "uploads"
          - name: ${PROJECT_NAME}
            mountPath: /var/www/html/wp-content/plugins
            subPath: "plugins"
          - name: ${PROJECT_NAME}
            mountPath: /var/www/html/wp-content/themes
            subPath: "themes"
      volumes:
        - name: ${PROJECT_NAME}
          persistentVolumeClaim:
            claimName: name-pvc-claim

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: name-pvc-claim
spec:
  volumeName: ${PROJECT_NAME}
  storageClassName: nfs
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi

The following files are required for build and deployment.

.env

DB_HOST=${DB_HOST}
DB_USER=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}
DB_NAME=${DB_NAME}

WORDPRESS_DB_HOST=${WORDPRESS_DB_HOST}
WORDPRESS_DB_USER=${WORDPRESS_DB_USER}
WORDPRESS_DB_PASSWORD=${WORDPRESS_DB_PASSWORD}
WORDPRESS_DB_NAME=${WORDPRESS_DB_NAME}
WORDPRESS_TABLE_PREFIX=${WORDPRESS_TABLE_PREFIX}

.gitlab-ci.yml

stages:
  - build
  - deploy
  - after_deploy

#Add build and deploy stages depending on your Kubernetes settings
build:
...

deploy:
 ...
  
after_deploy:
  stage: after_deploy
  image: ${DOCKER_IMAGE}
  only:
    - main
  script:
    - NAME_WP_POD=$(kubectl get pods --sort-by=.metadata.creationTimestamp | awk '{print $1}' | awk 'END{print}')
    - STATUS_POD=$(kubectl get pods ${NAME_WP_POD} --no-headers -o custom-columns=":status.phase")
    - echo $STATUS_POD
    - echo $NAME_WP_POD
    - while [ "$STATUS_POD" != "Running" ]; do sleep 5; NAME_WP_POD=$(kubectl get pods --sort-by=.metadata.creationTimestamp | awk '{print $1}' | awk 'END{print}'); STATUS_POD=$(kubectl get pods ${NAME_WP_POD} --no-headers -o custom-columns=":status.phase"); done;
    - NAME_WP_POD=$(kubectl get pods --sort-by=.metadata.creationTimestamp | awk '{print $1}' | awk 'END{print}')
    - kubectl exec ${NAME_WP_POD} -- bash -c "chown -R www-data:www-data /var/www/html/wp-content/"

When changes are received in the branch (in this case, main), build and deployment will start.

After deployment, the newly launched pod is defined and the owner of the folder /var/www/html/wp-content/ will be changed to www-data.

Add a backup script.

This script is used for site backup.

#!/bin/bash

set -e

export DEBIAN_FRONTEND=noninteractive

apt-get update
apt-get install awscli -y
apt-get install default-mysql-client -y

export TIMESTAMP=$(date +'%Y%m%d')

mysqldump -h $WORDPRESS_DB_HOST -u $WORDPRESS_DB_USER -p$WORDPRESS_DB_PASSWORD --no-tablespaces $WORDPRESS_DB_NAME > backupdb_$TIMESTAMP.sql

tar cvzf backup$TIMESTAMP.tar.gz /var/www/html/wp-content/uploads /var/www/html/wp-content/plugins /var/www/html/wp-content/themes backupdb_$TIMESTAMP.sql

aws s3 cp ./backup$TIMESTAMP.tar.gz s3://$S3_BUCKET

The script creates an archive from copies of the folders and a dump of the database.

In this case, we use the same docker image for the cron job and deploying the WP site.

If your OS is not Debian 11 (like in this example) you have to change the install method awscli and mysql-client suitable for your OS.

Start CI/CD

Make sure that the CI/CD works without errors.

 

This command helps you verify that all objects exist and work properly.

kubectl get pod
NAME                           READY           STATUS      RESTARTS   AGE
name-pod-758f8c5468-pmr9w      1/1             Running     0          18s
kubectl get pvc
NAME             STATUS   VOLUME         CAPACITY   ACCESS MODES   STORAGECLASS   AGE
name-pvc-claim   Bound    name-volume    100Gi      RWX            nfs            18s

Now we need to copy the folders (plugins, themes, uploads)  to Persistent Volume Claim and assign the owner to this folder user www-data

Use the following commands:

kubectl cp ./wp-content/themes/ ${NAME_WP_POD}:/var/www/html/wp-content/themes/
kubectl cp ./wp-content/uploads/ ${NAME_WP_POD}:/var/www/html/wp-content/uploads/
kubectl cp ./wp-content/plugins/ ${NAME_WP_POD}:/var/www/html/wp-content/plugins/
kubectl exec ${NAME_WP_POD} -- bash -c "chown -R www-data:www-data /var/www/html/wp-content/"

Kubernetes cron job for backup

Create a user in AWS IAM with permission to write to the S3 bucket and an s3 bucket for storing backup archives.

Before creating a secret, check if the user can have enough permissions to copy files to the s3 bucket.

Manually create a Secret containing multiple key-value pairs

Example:

kubectl create secret generic name-your-secret --from-literal=S3_BUCKET='value'  --from-literal=AWS_ACCESS_KEY_ID='value' --from-literal=AWS_SECRET_ACCESS_KEY='value'

For backup add the following manifest to the deployment.yml file with describes a Kubernetes cron job.

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: ${PROJECT_NAME}
spec:
  successfulJobsHistoryLimit: 2 
  failedJobsHistoryLimit: 1     
  schedule: "0 23 * * 5"                   #Set the value you want
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: ${PROJECT_NAME}
            image: ${REPOSITORY_IMAGE}
            envFrom:
            - secretRef:
                name: name-your-secret     #Set the value of the generated secret
            env:
            - name: WORDPRESS_DB_HOST
              value: ${WORDPRESS_DB_HOST}
            - name: WORDPRESS_DB_USER
              value: ${WORDPRESS_DB_USER}
            - name: WORDPRESS_DB_PASSWORD
              value: ${WORDPRESS_DB_PASSWORD}
            - name: WORDPRESS_DB_NAME
              value: ${WORDPRESS_DB_NAME}
            imagePullPolicy: IfNotPresent
            args: 
            - /bin/bash
            - -c
            - bash /opt/scripts/backup.sh;
            volumeMounts:
            - name: ${CI_PROJECT_NAME}
              mountPath: /var/www/html/wp-content/uploads
              subPath: "uploads"
            - name: ${CI_PROJECT_NAME}
              mountPath: /var/www/html/wp-content/plugins
              subPath: "plugins"
            - name: ${CI_PROJECT_NAME}
              mountPath: /var/www/html/wp-content/themes
              subPath: "themes"
          
          volumes:
          - name: ${PROJECT_NAME}
            persistentVolumeClaim:
              claimName: name-pvc-claim      

In this manifest, we assign the same persistent volume claim as for the site’s WP with the same volumes. It is necessary in order to be able to make a copy of the folders.

Make changes and check if the cron job was created.

kubectl get cronjob
NAME          SCHEDULE     SUSPEND   ACTIVE   LAST SCHEDULE   AGE
name-cronjob  0 23 * * 5   False     0        0               15s

In the cron job, edit the schedule to start the script every 10 minutes to check how the script works (for testing purposes). 

kubectl patch name-cronjob -p '{"spec":{"schedule": "*/10 * * * *"}}'

Check the created pods and their logs

kubectl get pod
NAME                        READY   STATUS      RESTARTS   AGE
name-pod-1677508500-5mcfn   0/1     Completed   0          5m
name-pod-758f8c5468-pmr9w   1/1     Running     0          1h

After the cron job works correctly, you should see the status of the job “Completed” and Redy “0/1”.

Now let’s check the logs of the pod with the cron job started.

kubectl log name-pod-1677508500-5mcfn

Below is a sample part of the logs that you should see when the pod is working properly.

/var/www/html/wp-content/themes/twentytwentythree/templates/archive.html
/var/www/html/wp-content/themes/twentytwentythree/templates/blank.html
/var/www/html/wp-content/themes/twentytwentythree/templates/index.html
/var/www/html/wp-content/themes/twentytwentythree/templates/home.html
/var/www/html/wp-content/themes/twentytwentythree/templates/blog-alternative.html
/var/www/html/wp-content/themes/twentytwentythree/templates/search.html
/var/www/html/wp-content/themes/twentytwentythree/templates/404.html
backupdb_20230303.sql
upload: ./backup20230303.tar.gz to s3://name-your-bucket/dname-your-folder/backup20230303.tar.gz

Also, do not forget to check the presence of the created archive in the s3 bucket.

After checking, set the value of the schedule to the previously set value.

kubectl patch name-cronjob -p '{"spec":{"schedule": "0 23 * * 5"}}'