s3-orchestrator #1682: run the demo backing services in Nomad and Kubernetes
s3-orchestrator presents many S3 and blob storage providers as a single S3 endpoint. It keeps a configurable number of copies across them with read failover, and can add per-backend byte, request, ingress and egress limits, zstd compression and envelope encryption.
My pull request #1682 was merged on October 15. It closes issue #265 and is my largest contribution so far: 1,928 lines added and 137 removed across 27 files.
The problem
The project ships two local demos, one on HashiCorp Nomad and one on Kubernetes (k3d). Both ran only the orchestrator itself in the scheduler. Everything it depends on, three MinIO backends, PostgreSQL and Redis, plus the observability stack of Prometheus, Grafana, Tempo, Loki and Alloy, still started through docker-compose.
As the issue put it, a Nomad demo should use Nomad, and a Kubernetes demo should use Kubernetes. The goal was to make each demo self-contained in the scheduler it shows off, while keeping docker-compose.test.yml for make run and make dev-deps.
The Nomad demo
- Backing services job (
services.nomad.hcl): PostgreSQL, Redis and three MinIO backends. They register in Nomad’s built-in service registry withprovider = "nomad"andaddress_mode = "driver", so the registry holds each container’s own address. The dev agent advertises and publishes ports on 127.0.0.1, which other containers can’t reach, so registering the host address would not have worked. The host ports match the ones compose used, bound to localhost, sopsql,redis-cliandmcon the host work the same way. - Bucket setup as a batch job (
minio-setup.nomad.hcl): a one-shot job that creates the backend buckets, using addresses from the registry. - Observability job (
observability.nomad.hcl): Prometheus, Grafana, Tempo, Loki and Alloy, with the same images as compose and thedeploy/monitoringconfigs bind-mounted. Prometheus, Tempo and Loki use the host network, as in compose, so Prometheus still discovers the orchestrator instances through the Nomad API. - Agent configuration (
agent.hcl): turns on memory oversubscription, so each task reserves little memory and bursts to itsmemory_maxunder load, and the whole stack still fits beside the orchestrator fleet on a laptop. - Orchestrator job: the config’s backing-service addresses are now
nomadServicetemplate lookups. A 30-second templatewaitmeans a service that briefly drops out of the registry while restarting never gets rendered into the config. demo.sh: submits the jobs, waits for healthy allocations andpg_isready, then runs the bucket setup. On a reused agent, it checks for bind-mount support and memory oversubscription before starting.
The Kubernetes demo
services.yaml: PostgreSQL, Redis and three MinIO backends in ademonamespace, with local-path persistent volume claims.minio-setup.yaml: the bucket setup as a Kubernetes Job, deleted and re-applied on each run.observability.yaml: Prometheus, Grafana, Tempo and Loki. Grafana, Prometheus and Tempo are LoadBalancer Services on ports 13000, 19090 and 3200, published through k3d’s load balancer.- Alloy reaches Loki and Prometheus by cluster DNS instead of the host gateway.
demo.sh: creates the cluster with the extra port mappings, loads thedeploy/monitoringconfigs into ConfigMaps with server-side apply, waits for the backing services and the bucket Job, and fills the shared config with cluster DNS names. It warns when a reused cluster is missing the new port mappings.
Shared changes
- One placeholder per address.
deploy/local/config.yamlhas a placeholder for each backing-service address, replacing the single__HOST_IP__. - A stricter config renderer.
render_configindeploy/local/lib.shtakesNAME=valuepairs and fails on any placeholder left unfilled. It avoids${var//}substitution, so it also works on the bash 3.2 that macOS ships. - A port guard.
refuse_running_dev_depsstops a demo from starting while the compose stack frommake dev-depsholds the same ports. - Environment-driven URLs. Grafana’s datasource URLs come from
PROMETHEUS_URL,LOKI_URLandTEMPO_URL, and Tempo’s remote-write URL fromPROMETHEUS_REMOTE_WRITE_URL, so the same monitoring configs work under compose, Nomad and Kubernetes.docker-compose.test.ymlsets the values it used before. - Docs.
deploy/README.md,docs/deployment.md,docs/monitoring.md, the web demo guide and the file headers that described the demos as compose-backed.
How I checked it
The change has no Go code, so the Go tests and linters didn’t apply. Before opening the pull request, I ran static checks on everything it touched:
- shellcheck is clean on both
demo.shscripts,lib.shand the bucket setup scripts. - Every Nomad file parses as HCL, including the orchestrator job as
demo.shrenders it. - With a stub
nomadService, the rendered config matchesdeploy/local/config.yamlapart from the addresses. - kubeconform in strict mode against Kubernetes 1.33 validates all 37 resources.
docker compose -f docker-compose.test.yml configpasses.render_configproduces identical output underBASH_COMPAT=31,BASH_COMPAT=42and native bash.
Review
The maintainer pulled the branch and tested it against Nomad 2.0.7 and the k3d demo, including the project’s make perf load scenarios. That hands-on run found problems static checks couldn’t, and I fixed each one in follow-up commits:
- Docker volumes are off in Nomad dev mode.
nomad agent -devdoesn’t enable Docker bind mounts, so every observability task failed to start.agent.hclnow turns them on. - Data was landing in RAM. The dev agent’s task directories live on tmpfs on most Linux machines, so data stored there counted against each container’s memory, and MinIO was OOM-killed partway through the multipart perf scenario. PostgreSQL and the three MinIO backends now keep their data in Docker named volumes, as compose did, and all seven perf scenarios pass.
- Teardown orphaned containers.
demo.sh downstopped the jobs and killed the agent before their containers had exited. It now waits for the containers and the agent, removes anything left over from an earlier run, and deletes the demo’s volumes. - One teardown path.
make cleanassumed compose still owned the backing services. It now callsdemo.sh down. - Formatting.
nomad fmt -recursive deploy/nomad/realigned the check blocks. - Grafana under load. With a dashboard open during
make perf, Grafana went past its 1 GiB limit, so both demos now let it burst to 2 GiB.
A final commit restored the executable bit on demo.sh, which an earlier commit had dropped. The maintainer confirmed everything from the review worked and merged the pull request on October 15, and also complimented using a Nomad batch job for the MinIO bucket setup.
Notes for anyone running the demos
- Stop
make dev-depsbefore starting a demo. The demos use the same host ports, and the scripts refuse to start while it is running. - On Nomad, a restarted backing service usually comes back at a new container address. Backend and database addresses don’t reload on
SIGHUP, so the orchestrator fleet restarts once, about 30 seconds later, to pick up the new address. - The Nomad demo still needs native Docker Engine on Linux, for host networking and
/var/run/docker.sock.
s3-orchestrator welcomes contributions, and the maintainer has suggested a few Go-side issues as good next steps for new contributors.