Kubernetes : Ghost blog
Bonjour à tous,
Aujourd'hui, un billet qui porte sur la mise en place de Ghost blog sur un cluster K8S avec un volume persistant sur NFS.
Contexte : à l'origine, le blog était sur une VM FreeBSD ce qui impliquait de maintenir l'OS, la version de node, les montées de version de ghost, etc.
Avec le passage sur un cluster K8S, la maintenance est facilitée dans ce cas et la DB étant sur un serveur dédié, le backup l'est aussi.
L'article qui m'a servi de support :
https://blog.groupe-creative.fr/comment-deployer-un-blog-ghost-avec-kubernetes/
Merci Maël!
J'ai rajouté quelques modifications notamment sur la partie volume/storage.
NFS
Vu qu'il s'agit d'un cluster et qu'il faut du stockage persistent, le mieux dans mon cas est de faire du NFS.
Pour cela, il faut un provider NFS qui sera utilisé ensuite lors des claim.
Depuis le master, j'ai utilisé le chart helm recommandé dans la documentation Kubernetes avec cette configuration :
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--set nfs.server=nfs.adm.securmail.fr \
--set nfs.path=/srv/k8s \
--set storageClass.onDelete=retain \
--set storageClass.pathPattern='/${.PVC.namespace}-${.PVC.name}'
Ces paramètres sont bien entendu en adéquation avec la configuration faite côté serveur NFS :
root@nfs:~# showmount -e localhost
Export list for localhost:
/srv/k8s 2a0e:f41:0:3::/64,192.168.0.0/24
On vérifie ensuite que la storage class est bien présente :
kubectl get storageclasses
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-client cluster.local/nfs-subdir-external-provisioner Delete Immediate true 9m37s
Blog
Namespace
apiVersion: v1
kind: Namespace
metadata:
name: ghost
Et on apply :
kubectl apply -f namespace.yml
Vérification que le namespace est présent :
kubectl get ns
NAME STATUS AGE
argocd Active 90d
default Active 162d
ghost Active 22h
ingress-nginx Active 162d
kube-flannel Active 162d
kube-node-lease Active 162d
kube-public Active 162d
kube-system Active 162d
kubernetes-dashboard Active 162d
nfs-provisioner Active 5h13m
puppet Active 138d
Volum claim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ghost-pvc
namespace: ghost
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs-client
resources:
requests:
storage: 1Gi
Et on apply :
kubectl apply -f volum_claim.yml
Vérification :
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-d0542f12-a5b8-47ee-88a2-5d7ff0d015d0 1Gi RWX Delete Bound ghost/ghost-pvc nfs-client 5h5m
Service
apiVersion: v1
kind: Service
metadata:
name: ghost-service
namespace: ghost
spec:
ports:
- name: ghost-http
port: 2368
selector:
app.kubernetes.io/name: ghost
On apply :
kubectl apply -f service.yml
Vérification :
kubectl get service -n ghost
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ghost-service ClusterIP 10.110.208.97 <none> 2368/TCP 5h1m
Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ghost-ingress
namespace: ghost
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
ingressClassName: nginx
rules:
- host: www.boris-tassou.fr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ghost-service
port:
number: 2368
Avec une option pour le body size, sans cette option, l'upload d'un thème par exemple peut ne pas fonctionner.
On apply :
kubectl apply -f ingress.yml
Vérification :
kubectl get ingress -n ghost
NAME CLASS HOSTS ADDRESS PORTS AGE
ghost-ingress nginx www.boris-tassou.fr 192.168.0.25 80 5h2m
Config map
apiVersion: v1
kind: ConfigMap
metadata:
name: ghost-config
namespace: ghost
data:
config.k8s.json: |
{
"url": "http://www.boris-tassou.fr",
"server": {
"port": 2368,
"host": "0.0.0.0"
},
"database": {
"client": "mysql",
"connection": {
"host": "mysql01r",
"port": 3306,
"user": "ghost",
"password": "BLABLABLA",
"database": "ghost"
}
},
"logging": {
"transports": [
"file",
"stdout"
]
},
"mail": {
"from": "blog@boris-tassou.fr",
"transport": "SMTP",
"options": {
"host": "SMTP",
"port": 25
}
},
"process": "systemd",
"paths": {
"contentPath": "/var/lib/ghost/content"
}
}
On apply :
kubectl apply -f config-map.yml
Vérification :
kubectl get cm -n ghost
NAME DATA AGE
ghost-config 1 5h45m
kube-root-ca.crt 1 22h
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: ghost
namespace: ghost
labels:
app.kubernetes.io/name: ghost
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ghost
template:
metadata:
labels:
app.kubernetes.io/name: ghost
spec:
volumes:
- name: ghost-pv-claim
persistentVolumeClaim:
claimName: ghost-pvc
- name: ghost-config
configMap:
name: ghost-config
items:
- key: config.k8s.json
path: config.k8s.json
containers:
- name: ghost
image: ghost:5.79.1-alpine
imagePullPolicy: Always
ports:
- name: http
containerPort: 2368
volumeMounts:
- mountPath: "/var/lib/ghost/content"
name: ghost-pv-claim
subPath: ghost
- name: ghost-config
mountPath: "/var/lib/ghost/config.production.json"
subPath: config.k8s.json
On apply :
kubectl apply -f deployment.yml
Vérification :
kubectl get deployment -n ghost
NAME READY UP-TO-DATE AVAILABLE AGE
ghost 1/1 1 1 5h45m
On peut vérifier que le contenu est bien sur le NFS :
root@nfs:/srv/k8s# ls
ghost-ghost-pvc
root@nfs:/srv/k8s# ls -Al
total 4
drwxrwxrwx 3 root root 4096 Feb 14 10:05 ghost-ghost-pvc
root@nfs:/srv/k8s# ls -Al ghost-ghost-pvc/
total 4
drwxrwxrwx 11 gizmo root 4096 Feb 14 10:05 ghost
root@nfs:/srv/k8s# ls -Al ghost-ghost-pvc/ghost/
total 36
drwxr-xr-x 2 gizmo gizmo 4096 Feb 12 22:54 apps
drwxr-xr-x 2 gizmo gizmo 4096 Feb 14 11:16 data
drwxr-xr-x 2 gizmo gizmo 4096 Feb 12 22:54 files
drwxr-xr-x 2 gizmo gizmo 4096 Feb 12 22:54 images
drwxr-xr-x 2 gizmo gizmo 4096 Feb 14 10:05 logs
drwxr-xr-x 2 gizmo gizmo 4096 Feb 12 22:54 media
drwxr-xr-x 4 gizmo gizmo 4096 Feb 14 11:16 public
drwxr-xr-x 2 gizmo gizmo 4096 Feb 14 10:10 settings
drwxr-xr-x 4 gizmo gizmo 4096 Feb 14 10:31 themes
Et que côté pod on est bon :
kubectl get pods -n ghost
NAME READY STATUS RESTARTS AGE
ghost-5bf66f6f59-c27qx 1/1 Running 0 61m
kubectl logs ghost-5bf66f6f59-c27qx -n ghost
[2024-02-14 10:16:02] INFO Ghost is running in production...
[2024-02-14 10:16:02] INFO Your site is now available on http://www.boris-tassou.fr/
[2024-02-14 10:16:02] INFO Ctrl+C to shut down
[2024-02-14 10:16:02] INFO Ghost server started in 1.018s
[2024-02-14 10:16:03] WARN Database state requires migration.
[2024-02-14 10:16:04] INFO Creating database backup
[2024-02-14 10:16:04] INFO Database backup written to /var/lib/ghost/content/data/boris-tassou-fr.ghost.2024-02-14-10-16-04.json
[2024-02-14 10:16:04] INFO Running migrations.
[2024-02-14 10:16:04] INFO Adding tokens.updated_at column
[2024-02-14 10:16:05] INFO Adding tokens.first_used_at column
[2024-02-14 10:16:05] INFO Adding tokens.used_count column
[2024-02-14 10:16:05] INFO Dropping table: suppressions
[2024-02-14 10:16:05] INFO Adding table: suppressions
J'ai un haproxy en amont avec cette configuration pour le backend :
backend bk_k8s_http
mode http
http-request set-header Host %[req.hdr(Host)]
server k8s k8s-cluster.adm.securmail.fr:80 check inter 1m
Le blog est maintenant accessible!
Comme amélioration possible, on pourrait rajouter un certificat interne et modifier l'ingress pour avoir du HTTPS entre haproxy et l'ingress.
Have fun.