Kubernetes with Helm
Deploy the CMS to a Kubernetes cluster with its Helm chart, against a managed Mongo and a managed Redis.
Why the chart writes the production rules down
A production CMS needs more than a container that starts. The database indexes have to be built before new pods take traffic. The probes have to tell a restart from a drain, a shutdown has to finish the requests already in flight, secrets have to stay out of the configuration, and the ingress must not cut a large upload. The chart writes those rules down once, so every deployment gets them by default instead of rediscovering each one during an incident.
What the chart renders
The chart deploys the same iegork/quillet image you run with Docker. By default Mongo and Redis are external, and the chart points at them with connection strings, because a managed database and a managed cache are what it is built for. For an evaluation install, optional Bitnami subcharts can stand both up inside the release.
A release is made of these parts:
- A Deployment of
replicaCountpods (one by default), or an autoscaler betweenautoscaling.minReplicasandmaxReplicas. Each pod runs as uid 1000 with a read-only root filesystem, a writableemptyDirat/tmpfor uploads and image transforms in progress, and a memory limit of 768Mi. That limit leaves room for one transform of a 50-megapixel image, which needs about 200 MB. - A ConfigMap built from
config,authandassets, with one environment variable per key. A change to it rolls the pods by itself, through achecksum/configannotation. - A Secret built from
secret, unless you name anexistingSecretof your own. Secrets never reach the ConfigMap. - The migration, as a Job that runs before install and before every upgrade. It builds the database indexes. Its
backoffLimitis0and its deadline is 120 seconds, so a failed or stuck migration stops the release instead of letting new pods reach a database whose indexes do not match. - The probes. Liveness is on
/healthz, which answers for as long as the process runs, a drain included. Readiness is on/readyz, which fails as soon as a shutdown begins or as soon as Mongo or Redis is unreachable. - The drain. A five-second
preStopsleep lets the Service stop routing to the pod before its socket closes; the pod then has up toSHUTDOWN_GRACE_MSfor requests in flight, all inside a 30-second termination grace period. The chart refuses to render a grace period that does not exceed the other two together. - An Ingress, when
ingress.enabledis on, withTRUST_PROXY=1for its one hop and an nginxproxy-body-sizederived fromconfig.assetMaxBytes. - A PersistentVolumeClaim for uploaded files under the default
diskbackend.
values.schema.json checks image.repository, config.baseUrl, config.trustProxy, ingress.host when the ingress is on, and the bucket's endpoint and name under s3. A release missing one of them fails at helm install, not later in a crash loop.
How to
The chart lives in the charts/quillet directory of the CMS source. It is not published to a chart repository, so the commands below run from a copy of that source.
- Create the Secret with the four values the CMS cannot start without. Keeping them out of your values file keeps them out of your history.
kubectl create secret generic quillet-secrets \
--from-literal=MONGO_URI='<mongodb+srv://user:password@cluster.example.net/quillet>' \
--from-literal=REDIS_URL='<rediss://default:password@cache.example.net:25061>' \
--from-literal=SESSION_SECRET="$(openssl rand -hex 32)" \
--from-literal=WEBHOOK_SECRET_KEY="$(openssl rand -base64 32)"
MONGO_URI must address a replica set, and every managed Mongo is one. A standalone mongod refuses the transactions the CMS writes with.
- Write a values file. This is the whole of a minimal one:
image:
repository: iegork/quillet
tag: '1.1.1'
ingress:
enabled: true
host: <cms.example.com>
tls:
issuer: <letsencrypt-prod>
config:
baseUrl: https://<cms.example.com>
existingSecret: quillet-secrets
ingress.host and config.baseUrl must name the same host, because the chart derives neither one from the other. tls.issuer names a cert-manager ClusterIssuer and adds its annotation to the ingress.
- Fetch the subcharts the chart declares, then install.
helm dependency build ./charts/quillet
helm install quillet ./charts/quillet -f values.yaml --wait
The migration runs first, and the pods start after it. Follow them with kubectl get pods. When the release is ready, curl -s https://<cms.example.com>/readyz prints {"status":"ok","mongo":"up","redis":"up"}.
- Open the dashboard at your host and create the first account. Registration is open on a new install. Close it afterwards with
config.authRegistration: closed, as Sign-in with GitHub or Google describes.
To run more than one pod, move files to a bucket first. Set assets.backend: s3 and the assets.s3 values, put the two bucket keys in the Secret as ASSET_S3_ACCESS_KEY_ID and ASSET_S3_SECRET_ACCESS_KEY, and only then raise replicaCount or turn on autoscaling. Under disk, the chart refuses more than one replica unless the claim is ReadWriteMany. File storage has the details.
Some variables have no value in the chart, such as ASSET_MIME_ALLOWLIST, ASSET_TRANSFORM_CONCURRENCY, IMPORT_MAX_BYTES or REDIS_KEY_PREFIX. Those go in extraEnv, which reaches both the pods and the migration:
extraEnv:
- name: IMPORT_MAX_BYTES
value: 512mb
To try the chart on a throwaway cluster with no managed services, turn on both subcharts and let the chart derive the connection strings:
helm install quillet ./charts/quillet \
--set image.repository=iegork/quillet --set image.tag=1.1.1 \
--set config.baseUrl=http://localhost:8080 \
--set mongodb.enabled=true --set mongodb.auth.rootPassword=<password> --set mongodb.auth.password=<password> \
--set redis.enabled=true --set redis.auth.password=<password> \
--set secret.sessionSecret="$(openssl rand -hex 32)" \
--set secret.webhookSecretKey="$(openssl rand -base64 32)"
Choose passwords without characters that need escaping in a URL, because the chart puts them inside the connection strings.
Limits
- The chart is not in a chart repository. You install it from a copy of the CMS source, and you upgrade it the same way.
- The subcharts are for evaluation only. They come with no backups and no monitoring, and their passwords sit in your shell history.
diskon block storage that isReadWriteOnce, such as DigitalOcean's, means exactly one replica and no autoscaling. Production with more than one pod meanss3.- An existing release left on
diskgains a PersistentVolumeClaim on its next upgrade. If you would rather the chart did not create one, setassets.disk.existingClaimto a claim you already have. - The ingress body limit is one number for every path, derived from
config.assetMaxBytes. Keep that value at or aboveconfig.jsonBodyLimit; Helm cannot compare the two spellings for you. - The chart adds no NetworkPolicy. Webhooks call whatever URLs owners configure, so a cluster that denies egress by default must allow TCP 443 from the CMS pods.
- Nothing checks what is inside an
existingSecretat install. A missing key, such as a bucket key unders3or a provider secret, shows up later as a pod that fails to boot. - To set
ASSET_SIGNING_SECRETfrom anexistingSecret, also setsecret.assetSigningSecretto any non-empty value. Otherwise the key stays out of the pods' environment. - Webhook signatures carry a timestamp that receivers check within five minutes, so the nodes need correct time.
Related
- Run with Docker runs the same image on one machine.
- Upgrades, migrations and health covers
helm upgrade, the migration and the probes in more depth. - Configuration reference lists every variable that the ConfigMap, the Secret and
extraEnvcan set. - Behind a proxy and TLS explains
TRUST_PROXYand the body limit at the ingress. - Backups keeps a copy of each project outside the cluster.