Building a Blog on Civo K3s
What better way to start learning a new technology than to use it for something productive? This guide will get you up and running with a
Ghost blog on Civo's managed Kubernetes platform. It will quickly go over Civo Kubernetes and getting started, followed by a step-by-step guide for configuring your blog.
Getting Up and Running With Kubernetes
At the time of writing Civo are only allowing a limited number of people to join the #KUBE100 Beta. There are still spaces on the beta, so it's definitely worth registering your interest.
To keep this guide focused to deploying Ghost, it will not cover setting up Kubernetes. To get up and running in Civo managed Kubernetes, take a look at some of the guides here, or Alex Ellis's post on his blog detailing his experience getting started. Once you have a cluster running, you can get started with deploying Ghost with the section below.
Make sure you also have
kubectl installed, as we will be managing the cluster using it.
I will run through the setup of Ghost on Civo, step by step. I would recommend using a real domain name throughout, it will make the SSL bit later easier. Otherwise, you will probably end up ripping it all down and starting again. Fortunately with Kubernetes that's not as painful as it seems!
First, let's download the configuration file of the cluster you will be working with. You will find this on the cluster information page on the Civo dashboard, like in the image below:
Open your favourite terminal and navigate to a sensible directory.
Once downloaded you can set your session to use this:
You can verify you are connected to the right cluster by doing a quick check of the nodes:
kubectl get nodes
And checking these against the cluster in the web interface, you wouldn't want to be connected to the wrong cluster!
Step 1 - Storage
To ensure the blog does not vanish like a...(sorry), I had to make sure there was persistent storage available for both the database and the ghost configuration/theme files. Civo Kubernetes provides stateful storage through an application:
The Civo marketplace provides the essential tools for Kubernetes and Longhorn by Rancher is one of them. Having not used Longhorn before, the installation was as easy as clicking the icon! Give it a few minutes to create the pods. You can check on their progress with, assuming you have
kubectl set up and pointing at your cluster as per the set-up instructions:
kubectl get pods -n longhorn-system NAME READY STATUS RESTARTS AGE svclb-longhorn-frontend-4z4zp 0/1 Pending 0 93s longhorn-manager-mgwrt 1/1 Running 0 94s longhorn-ui-7bd887cd87-qkng2 1/1 Running 0 93s engine-image-ei-3827e67c-sthtc 1/1 Running 0 57s instance-manager-e-d25cac90 1/1 Running 0 8s instance-manager-r-225aa2b5 1/1 Running 0 8s longhorn-driver-deployer-576bc5f794-ssfb9 0/1 PodInitializing 0 93s
To speed up the process I've provided links below to some yaml files to create parts of the config. You may want to download and create these manually. They are provided for reference only.
Step 2 - Creating storage for MYSQL
You can use the command below to quickly deploy the PVC (Persistent Volume Claim):
kubectl apply -f https://raw.githubusercontent.com/keithhubner/CIVO-K3S/master/vol-mysql.yml
You should be able to view the PV (Persistent Volume) and PVC (Persistent Volume Claim):
kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-e14f138c-d070-4552-9cda-c2c37aad7680 5Gi RWO Delete Bound default/mysql-pv-claim longhorn 112m
kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE mysql-pv-claim Bound pvc-e14f138c-d070-4552-9cda-c2c37aad7680 5Gi RWO longhorn 113m
Step 3 - Creating the storage for Ghost
Again I have a yaml file ready to go to create this for you. If you want to, feel free to download and edit this file yourself.
kubectl apply -f https://raw.githubusercontent.com/keithhubner/CIVO-K3S/master/vol-ghost.yml
Again check the PV and PVC have been setup correctly. They should look a bit like this:
kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-e14f138c-d070-4552-9cda-c2c37aad7680 5Gi RWO Delete Bound default/mysql-pv-claim longhorn 116m pvc-ffbf04ab-295d-420e-85ce-7ba25e5ea7cd 5Gi RWO Delete Bound default/ghost-pv-claim longhorn 75m kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE mysql-pv-claim Bound pvc-e14f138c-d070-4552-9cda-c2c37aad7680 5Gi RWO longhorn 114m ghost-pv-claim Bound pvc-ffbf04ab-295d-420e-85ce-7ba25e5ea7cd 5Gi RWO longhorn 72m
Step 4 - Starting up MYSQL
Before deploying mysql, it's a good idea to create a Kubernetes secret to store the root password.
First we need to generate the password in base64, change this for your password:
echo -n some_text_to_encode | base64
You will get an output in base64:
Copy the code below to a file called mysql-pass.yml (make sure you change the password for the base64 value given above)
apiVersion: v1 kind: Secret metadata: name: mysql-pass type: Opaque data: password: c29tZV90ZXh0X3RvX2VuY29kZQ==
kubectl apply -f mysql-pass.yml
You should get a confirmation that the secret was created and you can check the secret has been stored:
kubectl get secrets NAME TYPE DATA AGE default-token-zcdl6 kubernetes.io/service-account-token 3 98m mysql-pass Opaque 1 53m
We will then reference this secret when both creating the server and also connecting to it from ghost, no passwords in code, hurrah!
Step 5 - Deploying MYSQL
You can use the command below to quickly deploy mysql:
kubectl apply -f https://raw.githubusercontent.com/keithhubner/CIVO-K3S/master/mysql-ghost.yml
After a few minutes you should see that the ghost-mysql pod is now running:
kubectl get pods NAME READY STATUS RESTARTS AGE ghost-mysql-797694cfb8-zqktc 1/1 Running 0 53m
It's worth checking that mysql is up and running and accepting connections (change the pod name to match your result from above):
kubectl logs ghost-mysql-x
Step 6 - Deploying Ghost
Before we deploy ghost, we need to setup Cert Manager to allow us to use TLS for our blog.
Using cert-manager and deploying it in Civo is pretty straight forward! As with Longhorn, you can deploy it directly from the Civo marketplace:
Alternatively, if you are using the Civo CLI you just need to run:
$ civo applications add cert-manager Added cert-manager v0.11.0 to Kubernetes cluster
Check it's started properly:
kubectl get pods -n cert-manager NAME READY STATUS RESTARTS AGE cert-manager-cainjector-54c4796c5d-zwbnz 1/1 Running 0 104m cert-manager-55fff7f85f-jzpxn 1/1 Running 0 104m cert-manager-webhook-77ccf5c8b4-2ggnz 1/1 Running 1 104m
Because Cert-Manager uses your domain records to verify ownership, you will need to alter your public DNS to point to the external IP address of one of your nodes (I appreciate a load balancer would be better here! Something i'll be writing about in a future blog!).
The first thing we need to do is create a
provider.yml file. Copy the file below into a new file called provider.yml making sure you** edit the email address to your own.**
apiVersion: cert-manager.io/v1alpha2 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: # The ACME server URL server: https://acme-v02.api.letsencrypt.org/directory # Email address used for ACME registration email: firstname.lastname@example.org # Name of a secret used to store the ACME account private key privateKeySecretRef: name: letsencrypt-prod # Enable the HTTP-01 challenge provider solvers: - http01: ingress: class: traefik
Next apply this file:
kubectl apply -f provider.yml
You will see:
You can use the copy the code below to a file called
yourbloghere domain with your own.
apiVersion: v1 kind: Service metadata: name: ghost-svc labels: app: ghost tier: frontend spec: selector: app: ghost tier: frontend ports: - protocol: TCP port: 2368 targetPort: 2368 --- apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: ghost-ingress annotations: cert-manager.io/cluster-issuer: letsencrypt-prod kubernetes.io/ingress.class: "traefik" ingress.kubernetes.io/ssl-redirect: "true" labels: app: ghost spec: tls: - hosts: - www.yourbloghere.com - yourbloghere.com secretName: letsencrypt-prod rules: - host: www.yourbloghere.com http: paths: - path: / backend: serviceName: ghost-svc servicePort: 2368 - host: yourbloghere.com http: paths: - path: / backend: serviceName: ghost-svc servicePort: 2368 --- apiVersion: apps/v1 kind: Deployment metadata: name: ghost-deploy spec: replicas: 1 selector: matchLabels: app: ghost tier: frontend template: metadata: labels: app: ghost tier: frontend spec: # securityContext: # runAsUser: 1000 # runAsGroup: 50 containers: - name: blog image: ghost imagePullPolicy: Always ports: - containerPort: 2368 env: - name: url value: https://www.yourbloghere.com - name: database__client value: mysql - name: database__connection__host value: ghost-mysql - name: database__connection__user value: root - name: database__connection__password valueFrom: secretKeyRef: name: mysql-pass key: password - name: database__connection__database value: ghost volumeMounts: - mountPath: /var/lib/ghost/content name: ghost-vol volumes: - name: ghost-vol persistentVolumeClaim: claimName: ghost-pv-claim
Then, apply this file to your cluster:
kubectl apply -f ghost.yml
You should see the following:
service/ghost-svc created ingress.networking.k8s.io/ghost-ingress created deployment.apps/ghost-deploy created
Again check the that it is running OK:
kubectl get pods NAME READY STATUS RESTARTS AGE ghost-mysql-797694cfb8-zqktc 1/1 Running 0 64m ghost-deploy-868ff9bfd5-z82t4 1/1 Running 0 44s
You can verify that both mysql and ghost are ready to rock with some simple commands. You can use the commands below, making sure to change the pod names to the ones returned by kubectl:
kubectl logs ghost-mysql-x
kubectl logs ghost-deploy-x
You will get lots of output, but should show the following:
2019-10-28 21:51:08 1 [Note] Server socket created on IP: '::'. 2019-10-28 21:51:08 1 [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. 2019-10-28 21:51:08 1 [Warning] 'proxies_priv' entry '@ root@ghost-mysql-797694cfb8-zqktc' ignored in --skip-name-resolve mode. 2019-10-28 21:51:08 1 [Note] Event Scheduler: Loaded 0 events 2019-10-28 21:51:08 1 [Note] mysqld: ready for connections. Version: '5.6.46' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
[2019-10-28 22:54:10] INFO Ghost is running in production... [2019-10-28 22:54:10] INFO Your site is now available on https://www.yourbloghere.com/ [2019-10-28 22:54:10] INFO Ctrl+C to shut down [2019-10-28 22:54:10] INFO Ghost boot 1.959s
You can also check if the certificate has been issued properly by using:
kubectl describe cert
All being well you should see the cert issued:
Status: Conditions: Last Transition Time: 2019-10-28T22:53:50Z Message: Certificate is up to date and has not expired Reason: Ready Status: True Type: Ready Not After: 2020-01-26T20:52:57Z Events: <none>
Now we can see if it's running, exciting!
Hopefully your DNS records have updated by now, if not just edit your host file to point to the new domain using an IP address of one of the nodes from the Civo web interface.
All being well you should see your shiny new blog:
Congratulations! Your new blog is ready!
If you have had any issues with this guide or would like any more detail on the process, please provide feedback. You can reach us on the Civo Community Slack or using the intercom chat on any civo.com page. Thanks!
A version of this post was originally posted on Keith's own blog, Technically Interesting.