How to run Ghost blog inside Kubernetes
For the very first post of this blog, we will see together how I installed Ghost.
Source files are available here.
Architecture
Ghost is a decoupled content management system allowing great flexibility.
Because its core is composed of a REST and a GraphQL API, we can plug in any technology to retrieve the publications and display them. By default, a turnkey interface is offered but nothing prevents you from using an Angular, React or Blazor frontend.
We're going to deploy it in Azure using Ghost's vanilla flavor. Follow me!
Deployment
I'm going to disregard the creation of the environment to not expand too much, but you must have a Kubernetes server in hands.
1 - Creating the namespace
This will allow you to isolate resources to facilitate management and cleaning.
kubectl create namespace blog
2 - Creating a volume for permanent storage
As Docker containers are volatile, the information contained in them will not be kept following a restart unless you associate a volume with them. We will let Kubernetes auto create an Azure File Share for our blog with a PersistentVolumeClaim.
#File : blog-persistent-volume-claim.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: blog-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: azurefile
resources:
requests:
storage: 50Gi
kubectl apply -f .\blog-persistent-volume-claim.yml --namespace=blog
3 - Creating a deployment
The deployment defines the specifications to be achieved for our Pods. Pods allow multiple containers to share the same context. Once deployed, a controller will monitor the current state of the resources to reach the desired specification. For example, if two Pods are requested and one is deleted, then a new Pod will be automatically be created.
#File : blog-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: blog
namespace: blog
labels:
app: blog
release: 3.38.2
spec:
replicas: 1
selector:
matchLabels:
app: blog
release: 3.38.2
template:
metadata:
labels:
app: blog
release: 3.38.2
spec:
volumes:
- name: blog-content
persistentVolumeClaim:
claimName: blog-pvc
containers:
- name: blog
image: ghost:3.38.2-alpine
env:
- name: url
value: <domain-name>
volumeMounts:
- name: blog-content
mountPath: /var/lib/ghost/content
resources:
limits:
cpu: "1"
memory: 256Mi
requests:
cpu: 100m
memory: 64Mi
ports:
- name: http
containerPort: 2368
protocol: TCP
restartPolicy: Always
kubectl apply -f .\blog-deployment.yml --namespace=blog
4 - Validation of deployment
Following a deployment, it is good practice to always check whether the operation went well.
kubectl get events --namespace=blog
kubectl get pods --namespace=blog
You should have a container ready.
NAME READY STATUS RESTARTS AGE
blog-679b759f94-kl87h 1/1 Pending 0 22s
5 - Creation of a service
We will now expose the Pods to an IP address internal to the cluster.
#File : blog-service.yml
apiVersion: v1
kind: Service
metadata:
name: blog
namespace: blog
spec:
type: ClusterIP
selector:
app: blog
ports:
- protocol: TCP
port: 80
targetPort: 2368
kubectl apply -f .\blog-service.yml --namespace=blog
6 - Creation of the certificate
We will be using Let's Encrypt, which allows you to generate trusted certificates for free.
You must first have created a certificate issuer called "letsencrypt-production". The easiest way to do this is to use cert-manager. Here is the procedure : https://cert-manager.io/docs/installation/kubernetes/
Create your certificate :
#File : blog-tls.yml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: blog-tls
spec:
secretName: blog-tls
dnsNames:
- <domain-name>
acme:
config:
- http01:
ingressClass: nginx
domains:
- <domain-name>
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
kubectl apply -f .\blog-tls.yml --validate=false --namespace=blog
7 - Creating an IngressController
The gateway to your services in Kubernetes will be an Nginx controller.
The good thing about it is that all traffic coming from outside is encrypted thanks to the certificate automatically published by Let's encrypt. Then, inside the cluster, we reduce the complexity of the services by only using the HTTP protocol.
Tanks to Ahmet Alp Balkan for the diagram
#File : blog-ingress.yml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: blog
namespace: blog
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/rewrite-target: /$1
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
tls:
- hosts:
- <domain-name>
secretName: blog-tls
rules:
- host: <domain-name>
http:
paths:
- path: /(.*)
backend:
serviceName: blog
servicePort: 80
kubectl apply -f .\blog-ingress.yml --namespace=blog
8- Domain name routing
All you have to do now is associate your domain name with the ClusterIp.
To do this, you will need to create an @ record and a * record with your registrar.
To get the public IP of the cluster:
kubectl get ingress --namespace=blog
Some more links
Kubernetes NodePort vs LoadBalancer vs Ingress? When should I use what?
Next
I plan to show how to backup and restore the Azure File Share. Tell me what you would like to see next! ;)