Ghost in the Cloud (Shell)

Nerd alert: op welke "moderne componenten" van Google Cloud draait deze website?

Ghost in the Cloud (Shell)
Ghost in the Shell (imdb), een controversiële live-action film van een controversiële Japanse animatie

Geïnspireerd door self-hosting Ghost on GCP dook ik voor een nieuwe website in de wereld van containers, docker files, secret managers, mailguns en cloud run. Behoorlijk enge termen wanneer je ze aan boord hoort, maar prima als het geen zeilweer is en je achter een scherm zit. Heerlijk om dan even de handen vies te maken, nekdiep in de code.

Wat hebben we allemaal nodig om het mooie CMS Ghost netjes in mijn favoriete cloud te draaien? Hieronder de stappen, die je met me kunt volgen in je eigen stukje cloud.

0. Werken vanuit Cloud Shell

Google biedt een gratis VM aan, een linux-omgeving geoptimaliseerd om naar hartelust los te gaan met alle componenten die je met een gcloud commando kunt bedienen. En nog veel meer, waaronder een online code-editor. Allemaal in je webbrowser, dus overal en op elk apparaat te gebruiken. Zie de productpagina voor meer info, of log direct in op shell.cloud.google.com.

We gebruiken de Cloud Shell eerst om een 'lokale' versie van het officiële image van Ghost te proberen. Later zullen we ermee een database en storage bucket maken en permissies instellen. Een eigen container image maken, opbouwen en online pushen. En daarna de Cloud Run-service uitrollen. Klinkt heftig? Begin hier maar eens mee:

docker pull docker.io/ghost:latest

docker run -d --name ghost -e NODE_ENV=development -p 8080:2368 ghost:latest

cloudshell get-web-preview-url -p 8080

Draai een verse Ghost-site vanuit het publieke image, binnen een paar minuten

Als je nét zo'n Docker-beginneling bent als ik, dan heb je misschien ook iets aan deze commando's:

  • Met docker exec -ti ghost sh start je een command line interface binnen de container.
  • In de container kun je met ghost logs en het handige ghost doctor zien wat er gebeurt.
  • Gebruik exit om uit de container te ontsnappen. En dan docker rm -f ghost om de hele container af te zinken, eh, op te ruimen. Ghost busters!

Tip: Vergeet niet dat alles wat je in de Cloud Shell instelt en opzet, na enige tijd vanzelf weer verdwijnt. Alleen je bestandjes worden er bewaard, dus sla wel dingen op.

1. Webservice in Cloud Run

De meest 'moderne' variant van Google's serverless & managed applicatie-oplossingen is Cloud Run. Niks zelf te beheren en schalen op netwerk- of machine-niveau, geen OS-beheer of load-balancing, geen gedoe met SSL-certificaten, etc. Gewoon je containertje met je (web)toepassing inpluggen, Google regelt de rest. En je kunt in een paar stapjes je eigen domeinnaam aan een Run-service ophangen, waarbij Google de SSL-certificaten regelt en je website daarna helemaal up and running is.

Om Ghost stabiel en vlot werkend te krijgen, lijkt het voor nu optimaal om minimaal 1 instantie te laten draaien. Dat betekent dat er altijd een website actief is, ook in die (vele) uren per dag dat we helemaal geen bezoekers krijgen. Ghost doet er nu eenmaal vrij lang over om 'op te starten', voordat de site beschikbaar is. "We'll be right back," zie je dan, iedere keer. We kiezen voor nu ook maximaal 1 instantie, een wat kleinere om potentiële kosten te beperken als er ineens duizenden bezoekers tegelijk komen. Maar dit kunnen we wel opschalen.

Ghost moet even voorgloeien, net als een dieselmotor

Wat gebruiken we verder om Ghost netjes op Cloud Run te draaien?

    • Een service-account, een afgebakende systeemgebruiker speciaal voor de Ghost-toepassing
    • Managed Secrets voor (o.a.) het wachtwoord van de database, want dat willen we netjes afschermen
    • De database die we hieronder instellen

Vanaf dit punt is het noodzaak dat we een projectje hebben draaien, een eigen stukje cloud waarin je aan de slag kunt. Maak daarin meteen even onder IAM het service-account aan, zoiets als ghost@<project>.iam.gserviceaccount.com. Kunnen we dat niet meer vergeten!

2. Database op Cloud SQL

Voor een Ghost-website in de ‘productie-omgeving’ hebben we een volwaardige database nodig. Eentje die natuurlijk ook lekker managed is, zodat we geen zorgen hebben over updates, dataverkeer, indexes en backups. Dat wordt dus een MySQL8-database in Cloud SQL. Super-schaalbaar en op kleine schaal heel betaalbaar.

Voor een website als deze, die niet meteen miljoenen bezoekers per dag bedient, kun je rustig kiezen voor de kleinste opzet, om kosten te drukken: in één zone op een gedeelde, kleine core, met een 10GB HDD, voor ongeveer $0.28 per dag. Voor nu is dit het meest kostbare component van de hele setup!

Klik de database-instantie in elkaar, of start 'm op met dit commando:

gcloud sql instances create ghost-db --availability-type=zonal \
--cpu=1 --memory=3840MiB --storage-size=10GB --storage-type=HDD \
--region=europe-west4 --root-password=<db_password> --async

Het wachtwoord moet natuurlijk ingewikkeld en uniek zijn. Dat onthouden we alleen even om het op te slaan in Secret Manager. Noem het daar maar db-password en geef het service-account er de Secret Accessor-rol op. En verbrandt daarna het briefje, waar je het toch stiekem opschreef.

Na een paar minuten draait je database-instantie. Maar ondertussen krijg je de connectie-naam, opgebouwd als <project>:<zone>:<instance>. In dit oer-Hollandse voorbeeld dus jouw-project:europe-west4:ghost-db. Die hebben we zo dadelijk weer nodig. Eerst nog even een database aanmaken binnen de instantie:

gcloud sql databases create ghost --instance=ghost-db

3. Media in Cloud Storage

De containers in Cloud Run kunnen altijd verdwijnen. (Net als op zee.) Ze komen wel vanzelf weer terug. (Op zee wil je daar niet over nadenken.) De nieuwe container is altijd vers, dus data die binnen de vorige was opgeslagen is weg. Voor alle gegevens die niet in de database terecht komen (met name ál die zeilfoto's) hebben we dus een andere opslag nodig. En daarvoor gebruiken we Google Cloud Storage, kortweg GCS. Ideaal voor dit soort content: oneindig schaalbaar, altijd beschikbaar, snel, en heel goedkoop. Het tegenovergestelde van een zeilboot.

Ghost heeft een prachtig systeem van adapters en voor GCS gebruik ik de ghost-gcs-adapter, redelijk recent en iets beter gedocumenteerd dan de versie van een gewaardeerde oud-collega. ;-)

Eerst maar eens de Cloud Storage bucket klaarzetten voor Ghost. Verzin er zelf een geschikte naam voor:

gsutil mb <bucket>

# Alles in de bucket wordt leesbaar voor bezoekers:
gcloud storage buckets add-iam-policy-binding  gs://<bucket> \
--member=allUsers --role=roles/storage.objectViewer

# Het serviceaccount van de Ghost mag de inhoud beheren:
gcloud storage buckets add-iam-policy-binding gs://<bucket> \
--member=<serviceaccount> \
--role=roles/storage.objectAdmin

Maak een Cloud Storage bucket, met de juiste permissies

4. GCS-adapter in de container

We moeten regelen dat de GCS-adapter ingeladen en gebruikt wordt in de container van Ghost. Daarvoor maken we een maatwerk-image dat doorbouwt op de meest recente, publieke versie. Maak het volgende bestand genaamd Dockerfile aan:

FROM ghost:5-alpine

# Add GCS Adapter, from https://github.com/danmasta/ghost-gcs-adapter
RUN mkdir -p /tmp/gcs "$GHOST_INSTALL/current/core/server/adapters/storage/gcs"; \
    wget -O - -q "$(npm view @danmasta/ghost-gcs-adapter dist.tarball)" | tar xz -C /tmp/gcs ; \
    npm install --prefix /tmp/gcs/package --omit=dev --omit=optional --no-progress ; \
    mv -v /tmp/gcs/package/* "$GHOST_INSTALL/current/core/server/adapters/storage/gcs"

# Use the Ghost CLI to set some pre-defined values.
RUN set -ex; \
    su-exec node ghost config storage.active gcs; \
    su-exec node ghost config storage.gcs.host "storage.googleapis.com"; \
    su-exec node ghost config storage.gcs.protocol "https"; \
    su-exec node ghost config storage.gcs.hash true; \
    su-exec node ghost config storage.gcs.hashAlgorithm "sha512"; \
    su-exec node ghost config storage.gcs.hashLength "16"; 

Een docker-opzetje mét de GCS-adapter en wat voorgebakken instellingen

Met deze dockerfile gaan we eerst eens lokaal een container opbouwen en proberen, door de bucket-naam als variabele mee te geven:

docker build . --tag ghost-gcs:latest

docker run -d -e NODE_ENV=development -e storage__gcs__bucket=<bucket> \
-p 8080:2368 ghost-gcs:latest

5. Eigen image, in Artifact Registry

Werkt het voorgaande image? Komt je Ghost-content daadwerkelijk in de bucket terecht? Jottem, dan kunnen we het image in ons werkproject registreren, voor gebruik in Cloud Run. Daarvoor maken we een repository aan en duwen we ons hernoemde image erin:

gcloud artifacts repositories create ghost --repository-format=docker \
--location=europe-west4 --description="My Ghost repo"

docker tag ghost-gcs europe-west4-docker.pkg.dev/<project>/ghost/ghost-gcs:latest

docker push europe-west4-docker.pkg.dev/<project>/ghost/ghost-gcs:latest

6. Uitrollen en varen, eh, rennen!

Inmiddels rest ons nog maar één ding: Cloud Run uitrollen!

In tegenstelling tot de meeste zeilen kun je een Cloud Run service uitrollen met de muis, via de console, zoals in de screenshots hierboven. Maar ook in één commando, waarin we al de eerder verzamelde info meegeven:

gcloud run deploy ghost \
--image=europe-west4-docker.pkg.dev/<project>/ghost/ghost-gcs:latest \
--set-env-vars=database__client=mysql \
--set-env-vars='database__connection__socketPath=/cloudsql/<project>:europe-west4:ghost-db' \
--set-env-vars=database__connection__database=ghost \
--set-env-vars=database__connection__user=root \
--set-env-vars=storage__gcs__bucket=<bucket> \
--set-cloudsql-instances=<project>:europe-west4:ghost-db \
--set-secrets=database__connection__password=db-password:latest \
--execution-environment=gen2 \
--port=2368 \
--service-account=ghost@<project>.iam.gserviceaccount.com \
--region=europe-west4 \
--project=<project>

Als alles goed gaat draait hierna Ghost volledig, in Cloud Run

De eerste keer dat Ghost in deze omgeving start zal het wel even duren voordat de database opgezet is en de Ghost-toepassing klaar is. Hou vooral de Cloud Run logs in de gaten, via console.cloud.google.com/logs.

RUN

Bijna klaar

Net als een boot is een website nooit af. En een blogpost als deze ook niet. Wat zou je nog kunnen verwachten?

  • Voor de stappen hierboven moet je eigenlijk eerst nog wat API's inschakelen, in je werkproject. Die stappen zou ik nog wel kunnen benoemen en toevoegen.
  • Als je jouw eigen domeinnaam gekoppeld hebt aan je Cloud Run service, wil je dat aan Ghost meegeven met de variabele url en misschien ook een aparte admin__url.
  • Ik wil nog wel een meertalig en/of maatwerk-Theme inladen in de Dockerfile. Is dat een nuttig vervolgbericht?
  • De logs van Cloud Run kunnen we gebruiken om site-bezoeken in beeld te brengen, met BigQuery en Looker Studio. Simpel om te maken, handig om te hebben, misschien nuttig om nog eens uit te schrijven.
  • Ik heb bewust de Mailgun-inrichting weggelaten uit het verhaal hierboven. Niet heel spannend eigenlijk. Gewoon een setje variabelen erbij en een extra secret voor het wachtwoord. Ping me maar als je er niet uitkomt.
  • Eigenlijk willen we met Cloud Build automatisch nieuwe versies uitrollen bij code-wijzigingen. Of zelfs automatisch bij nieuwe releases van Ghost, de adapter en het thema? En misschien maak ik mijn image nog wel publiek!
  • Momenteel werkt de nieuwe (momenteel in preview) Cloud Storage volume mounts (nog) niet voor Ghost. (Dat zou het allemaal ook wel héél makkelijk maken.)