Moving Wordpress to Civo K3s

Introduction

Moving your web site to a new provider is a bit like moving house: you don't know where to start, stuff breaks along the way and you vow never to do it again! I've been running a web site on a few cloud providers for a while now, but sometimes you just get the itch to play with something new...

So when a new provider comes onto the scene my brain says... "oh look at that shiny new hosting platform! Look how cheap it is! Look how much fun (late nights and alcohol) you could have moving your Wordpress site to it!"

Along came Civo.....

Following up from my previous post (Building a blog on Civo k3s) I was so impressed by not just what these guys were trying to do from a technology point of view but the passion they have for their service and helping their customers. Also, moving from a traditional web server to Kubernetes was too tempting to resist! Talk about that shiny new thing...

Building the new site

To follow along with this guide, you will need to have access to Civo's managed Kubernetes service. You can sign up here - the service is currently in beta so is accepting applicants through a form. If you get accepted to the beta, you will get free credit for the duration in return for your feedback on the service.

If you have an existing Wordpress site, this guide will walk you through moving it to the new platform, but it can also be used to deploy a fresh Wordpress installation on Kubernetes, with SSL certification.

Pre-requisites (Both in the CIVO marketplace):

  • Longhorn (storage)

  • Cert-manager (SSL certificate)

I have also assumed some prior knowledge of using k3s on Civo, I recommend you work through my previous blog if you have not done so already.

First, start up a cluster of your choice of size - the default is a 3-node cluster composed of Medium-sized nodes which will be plenty.

Then, let's create a namespace to put the new site into:

kubectl create namespace wordpress

Creating the volumes

Let's create the 2 volumes used for mysql server storage and the wordpress files, you can apply them directly using the following commands, but their contents are produced below as well for you to save locally and deploy:

kubectl apply -f https://raw.githubusercontent.com/keithhubner/civo-k3s-wordpress/master/vol-mysql.yml

kubectl apply -f https://raw.githubusercontent.com/keithhubner/civo-k3s-wordpress/master/vol-wordpress.yml

vol-mysql.yml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-mysql-pv-claim
  namespace: wordpress
  labels:
    type: longhorn
    app: wordpress
spec:
  storageClassName: longhorn
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

vol-wordpress.yml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-pv-claim
  namespace: wordpress
  labels:
    type: longhorn
    app: wordpress
spec:
  storageClassName: longhorn
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

You can check these have been created by doing the following:

kubectl get pvc --all-namespaces
NAMESPACE   NAME                       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
wordpress   wordpress-mysql-pv-claim   Bound    pvc-09aff171-dfe9-4cd0-a5be-024244a78348   5Gi        RWO            longhorn       55s
wordpress   wordpress-pv-claim         Bound    pvc-b2f7ecda-2d09-467f-8b2b-d5e625bb7365   5Gi        RWO            longhorn       44s
kubectl get pv
pvc-285dcc0c-6a10-4428-943e-8c0726feae32   5Gi        RWO            Delete           Bound    default/wordpress-mysql-pv-claim   longhorn                74s
pvc-d6f27478-56b2-402a-93c9-cfbdcbbe67b7   5Gi        RWO            Delete           Bound    default/wordpress-pv-claim         longhorn                53s

We will also need a secret to be used by mysql (make sure you change the some_text_to_encode)

Make a note of this password, you will need it later

echo -n some_text_to_encode | base64

You will get something like the following:

c29tZV90ZXh0X3RvX2VuY29kZQ==

Copy and paste the below into a new file called mysql-secret.yml (remembering to replace the password value with your base64 value generated in the previous step):

apiVersion: v1
kind: Secret
metadata:
  name: mysql-pass
  namespace: wordpress
type: Opaque
data:
  password: c29tZV90ZXh0X3RvX2VuY29kZQ==

And apply the file:

kubectl apply -f mysql-secret.yml

Now let's deploy mySQL

kubectl apply -f https://raw.githubusercontent.com/keithhubner/civo-k3s-wordpress/master/mysql-wordpress.yml

For reference, the contents of the above file mysql-wordpress.yml are:

apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  namespace: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: wordpress-mysql
  namespace: wordpress
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - image: mysql:5.6
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: wordpress-pv-claim

All being well you should have a lovely new mysql pod:

kubectl get pods -n wordpress
wordpress-mysql-56b57b8d45-tgd7p   1/1     Running   0          26s

And checking mysql is happy (replacing the pod name with yours):

kubectl logs -n wordpress wordpress-mysql-56b57b8d45-tgd7p
........
2020-02-19 21:03:05 1 [Note] mysqld: ready for connections.

Deploying Wordpress:

You will need to alter the scripts below replacing the site name with one of your chosing. In order for cert-manager to issue a real certificate, this will need to be a domain you own and can point the DNS record at the public IP.

First we will need to create the cert manager cluster issuer:

Staging certificate allows you to test the configuration before pointing live DNS records
Create the following file as issuer-staging.yaml on your local machine.

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging-wp
spec:
  acme:
    # The ACME server URL
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: someone@somewhere.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-staging-wp
    # Enable the HTTP-01 challenge provider
    solvers:
    - http01:
        ingress:
          class: traefik

Then, run kubectl apply -f issuer-staging.yaml to apply this to your cluster.

DO NOT INSTALL WORDPRESS IF YOU ARE MIGRATING FROM ANOTHER SERVER

If you are migrating your blog from another host, you will not need to re-create the files as you will be bringing these over.

Below is the manifest template to create the wordpress pod. Copy and paste this into your favourite editor and amend the values as required.

You can also get the file here
deploy-wordpress.yml.

You will need to match the wordpress version to the version you are running in your live site

apiVersion: v1
kind: Service
metadata:
  namespace: wordpress
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: wordpress-ingress
  namespace: wordpress
  annotations:
    kubernetes.io/ingress.class: "traefik"
    cert-manager.io/cluster-issuer: letsencrypt-prod-wp
    ingress.kubernetes.io/ssl-redirect: "true"
  labels:
    app: wordpress
spec:
  tls:
  - hosts:
    - www.somesite.com
    - somesite.com
    secretName: letsencrypt-prod-wp
  rules:
  - host: www.somesite.com
    http:
      paths:
      - path: /
        backend:
          serviceName: wordpress
          servicePort: 80
  - host: somesite.com
    http:
      paths:
      - path: /
        backend:
          serviceName: wordpress
          servicePort: 80
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  namespace: wordpress
  name: wordpress
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - image: wordpress:5.3.2-php7.3-apache
        name: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-mysql
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: wordpress-mysql-pass
              key: password
        ports:
        - containerPort: 80
          name: wordpress
        volumeMounts:
        - name: wordpress-persistent-storage
          mountPath: /var/www/html
      volumes:
      - name: wordpress-persistent-storage
        persistentVolumeClaim:
          claimName: wordpress-pv-claim

If all is well in the world you should be able to see the pod is now running:

kubectl get pods -n wordpress

wordpress-776d9476c8-rs679         1/1     Running   0          47s

You should also be able to see cert-manager doing its thing:

kubectl get cert -n wordpress

NAME                    READY   SECRET                AGE
letsencrypt-staging-wp   True                        35s

By either visiting the cluster's public IP, pointing your hosts file or updating your DNS records to point to the public IP of your cluster, you should see the Wordpress setup page that will look a little bit like this:

Next we need to create the secret which will be used for the database. Again you can look at this post for instructions on how to do this.

Once the storage has been created we can deploy mysql:

kubectl apply -f mysql-wordpress.yml

You can copy and paste the code below to a file called wordpress.yml to get yourself a templated wordpress service:

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: wordpress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/ingress.class: "traefik"
    ingress.kubernetes.io/ssl-redirect: "true"
  namespace: default
spec:
  tls:
  - hosts:
    - www.yoursitehere.com
    - yoursitehere.com
    secretName: letsencrypt-prod
  rules:
  - host: www.yoursitehere.com
    http:
      paths:
      - path: /
        backend:
          serviceName: wordpress
          servicePort: http
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - image: wordpress:5.2.3-php7.1-apache
        name: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-mysql
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 80
          name: wordpress
        volumeMounts:
        - name: wordpress-persistent-storage
          mountPath: /var/www/html
      volumes:
      - name: wordpress-persistent-storage
        persistentVolumeClaim:
          claimName: wordpress-pv-claim

If everything has gone to plan, then you should be able to reach the Wordpress installation page by editing your local host file with the domain name used earlier and public IP on the CIVO master node.

DO NOT INSTALL WORDPRESS WITH THE ABOVE INSTRUCTIONS IF YOU ARE MIGRATING FROM ANOTHER SERVER

You will notice the SSL certificate isn't quite right yet as we are using the staging issuer!

Before we re-point your live DNS and setup the proper certificate, we need to import your current site.

Where your current site is hosted will determine your export/import process. For this example I'll take an existing site I have root access to to backup and copy both the Wordpress files and the mysql database.

Backing up Wordpress Files

The easiest way to backup the Wordpress web files is to ZIP up the whole html folder.

Again this may vary depending on your server.

zip -r /tmp/wordpress.zip /var/www/html/

This will zip up the whole site into a wordpress.zip file in the /tmp folder. You will need to copy this file to your local machine. How to to do this is deyond the scope of this post, but I used a tool called CyberDuck for Mac.

Backing up MySQL

Again the process to backup the database will vary depending on your situation. I will be using the CLI to take a backup of the wordpress database and assuming the database name is wordpress.

mysqldump -u root -p wordpress > wordpress.sql
cp wordpress.sql /tmp/wordpress.sql

Again, copy this file to your local machine.

At this point I recommend either shutting down your old site or blocking any updates, otherwise you risk not having an up to date version in CIVO. There is a way to put your blog into "maintenance mode" in the dashboard, meaning people will not be able to post comments, for example.

Copying files into your containers

We will need to copy these backup files into the respective containers.

MySQL Restore

You will need to make a note of your mysql pod name:

kubectl get pods -n wordpress

NAME                               READY   STATUS    RESTARTS   AGE
wordpress-mysql-56b57b8d45-kht26   1/1     Running   0          5d12h
wordpress-776d9476c8-jjs4d         1/1     Running   0          5d12h

Next copy the SQL backup file into the pod from the directory you saved it:

kubectl cp /Users/keithhubner/Documents/wp_backup.sql wordpress/wordpress-mysql-56b57b8d45-kht26:/tmp/wp_backup.sql

Next restore the data into SQL:

Note your root password is the one which you set in the secret earlier

mysql -u root -p < wp_backup.sql

You can log into mysql and check the data has restored:

mysql -u root -p

USE wordpress;

SHOW tables;

select * from wp_users;

This should hopefully return some data you recongnise from your old wordpress setup.

Wordpress Web Restore

Again we need to copy in the backup file, remember to change the pod name:

kubectl cp /Users/keithhubner/wp_backup.zip wordpress/wordpress-776d9476c8-jjs4d:/tmp/wp_backup.zip

And unzip the files

The unzip package is not installed as part of the image, so you will need to install it. This will only be installed for the lifetime of the pod, but we only need it once.

apt-get install unzip

unzip wp_backup.zip -d /

You should now see all your files restoring into the wordpress html folder.

All being well, when you navigate to your site, you should see your site restored. Again you will get certificate warnings/issues until we issue the real certificate.

Re-pointing LIVE

Due to the time it takes to re-point DNS and setup the live cert make sure you plan for some downtime.

Once you are happy the site has been restored, you can re-point your public DNS records to the new IP. This is done through your domain registrar once you log in to manage your domains.

Check your DNS has updated:

nslookup www.yoursite.com 8.8.8.8

This should return the new CIVO public IP

We can now issue the new live certificate, amending the values as needed. Let's create a file called issuer-prod.yaml with the following contents:

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-wp
  namespace: wordpress
spec:
  acme:
    # The ACME server URL
    server: https://acme-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: someone@something.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-prod-wp
    # Enable the HTTP-01 challenge provider
    solvers:
    - http01:
        ingress:
          class: traefik

Then, you guessed it, apply it to your cluster:

kubectl apply -f issuer-prod.yaml

You can check the status of the certificate using the following command:

kubectl get cert -n wordpress

NAME                  READY   SECRET                AGE
letsencrypt-prod-wp   True    letsencrypt-prod-wp   5d12h

And you should see it issued successfully:

kubectl describe cert letsencrypt-prod-wp -n wordpress

...
 Last Transition Time:  2020-03-04T21:13:07Z
    Message:               Certificate is up to date and has not expired
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2020-06-02T20:13:06Z
Events:                    <none>
kubectl describe clusterissuer letsencrypt-prod-wp
Status:
  Acme:
    Last Registered Email:  keith@hubner.co.uk
    Uri:                    https://acme-v02.api.letsencrypt.org/acme/acct/79741635
  Conditions:
    Last Transition Time:  2020-03-04T21:08:53Z
    Message:               The ACME account was registered with the ACME server
    Reason:                ACMEAccountRegistered
    Status:                True
    Type:                  Ready
Events:                    <none>

Now you should be able to browse to your web address (removing any local host file records) and see a nice trusted certificate and your site!

Let me know on Twitter if you found this guide useful, and follow Civo for updates on their service as well.