First Post
Deploying Ghost was a little silly - I was chasing ghosts on some configuration issues with the Bitnami helm chart and decided I'd rather put together a ConfigMap
that has any normal configurations with only secrets pulled in from environment variables. This makes it easier to inspect with ghost config get...
, at least. One thing I also didn't enjoy was their take on initial setup - either the helm chart is initializing or it is updating, but you end up running into problems if you don't turn off the wizard-filler after the first run.
The result, here, is a snippet from my ansible that pushes out the StatefulSet
for Ghost. The key bit is the initContainers
sidecar that queries the Ghost API.
- name: Deploy Ghost
kubernetes.core.k8s:
state: present
force: True
namespace: "{{ ns }}"
name: ghost
api_version: apps/v1
kind: StatefulSet
definition:
metadata:
labels:
app: ghost
spec:
ports:
- port: 2368
name: http
clusterIP: None
selector:
matchLabels:
app: ghost
template:
metadata:
labels:
app: ghost
spec:
volumes:
- name: conf
configMap:
name: "{{ configmap }}"
items:
- key: config.production.json
path: config.production.json
initContainers:
- name: setup-wizard-filler
image: registry.gitlab.com/gitlab-ci-utils/curl-jq
command:
- /bin/sh
args:
- -ec
- |
until \
curl \
--silent \
--head \
--output /dev/null \
--fail \
https://{{ url }}/ghost/api/admin/authentication/setup/
do
echo "Waiting for server to come up"
sleep 3
done
body="$(
jq . <<EOF
{
"setup": [
{
"name": "{{ user }}",
"email": "{{ email }}",
"password": "${GHOST_PASSWORD}",
"blogTitle": "{{ blog_title }}"
}
]
}
EOF
)"
curl \
--silent \
--fail \
https://{{ url }}/ghost/api/admin/authentication/setup/ \
| jq .setup[].status \
| tr '[A-Z]' '[a-z]' \
| xargs -I% test 'false' = '%' \
&& curl \
--silent \
-H "Content-Type: application/json" \
-H "Cache-Control: no-cache" \
--data "${body}" \
https://{{ url }}/ghost/api/v3/admin/authentication/setup/ \
|| echo "No need for initialization - carry on."
echo "done!"
while :; do
echo "Nothing to do - try to sleep forever"
sleep 2073600
done
env:
- name: GHOST_PASSWORD
valueFrom:
secretKeyRef:
name: "{{ ghost_creds }}"
key: ghost-password
restartPolicy: Always
containers:
- name: ghost
image: "{{ ghost_image }}"
securityContext:
runAsUser: 1000
runAsGroup: 1000
command:
- ghost
args:
- run
- -V
- -d
- /var/lib/ghost
volumeMounts:
- name: "{{ volume }}"
mountPath: /var/lib/ghost/content
- name: conf
mountPath: /var/lib/ghost/config.production.json
subPath: config.production.json
env:
- name: NODE_ENV
value: production
- name: database__connection__password
valueFrom:
secretKeyRef:
name: "{{ mysql_creds }}"
key: mysql-password
- name: mail__options__auth__pass
valueFrom:
secretKeyRef:
name: "{{ ghost_creds }}"
key: smtp-password
ports:
- containerPort: 2368
name: http
protocol: TCP
startupProbe:
failureThreshold: 30
periodSeconds: 5
httpGet:
port: http
httpHeaders:
- name: X-Forwarded-Proto
value: https
path: /
readinessProbe:
initialDelaySeconds: 10
periodSeconds: 5
httpGet:
port: http
httpHeaders:
- name: X-Forwarded-Proto
value: https
path: /favicon.ico
volumeClaimTemplates:
- metadata:
name: "{{ volume }}"
spec:
storageClassName: "{{ storageclass }}"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 80Gi
vars:
ghost_image_name: ghost
# 5.104.1
ghost_version: "@sha256:9229a78a51c55b25f2b495153a0aee59f0dfcb84f835abd8be19dcf6f36ab792"
ghost_image: "{{ ghost_image_name }}{{ ghost_version }}"
volume: ghost-vol
storageclass: csi-driver-lvm-linear
There are two endpoints required for this operation:
- Setup status:
https://{{ url }}/ghost/api/admin/authentication/setup/
- Setup:
https://{{ url }}/ghost/api/v3/admin/authentication/setup/
The first endpoint, the status url for setup, returns some json with the jsonpath {.setup.status}
set to true
when setup is complete and false
otherwise. The second is where the web form submits user choices on initial setup. At this point, it's a simple matter of querying the first endpoint until it returns a valid response, sending the registration body to the second when it is available if no setup has been done yet.
The registration body contains the user's name, email, password, and desired title for the Ghost installation in the following schema:
{
"setup": [
{
"name": "{{ user }}",
"email": "{{ email }}",
"password": "${GHOST_PASSWORD}",
"blogTitle": "{{ blog_title }}"
}
]
}
Afterward, the setup sidecar kinda dangles uselessly, so it's put into an infinite loop with a really long sleep
.
After I get this all situated, the hope is definitely to organize it first into a nice little helm chart and then an operator with an attendant helm chart to try to smooth over some of Ghost's idiosyncrasies.
Anyway, Ghost is in the timekube.