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.