Loading the location seed set¶
How to publish a generated location seed set to GCS and load it into a target Postgres on demand. The seed set is the location backbone: countries, the admin-area hierarchy, the city gazetteer, and curated collections (UNESCO, Michelin, 50 Best). Loading it populates locations + related tables so discovery and search have data to resolve against.
The pipeline has four stages, three of which live here in devops:
app catalogseeder
seed-publish.sh
gs://…/seed/<version>
on-demand, per env
tomoda-{dev,prod}
Prerequisites¶
- Seed bucket + loader SA exist. Terraformed in
infrastructure/gcp/location_seed.tf: buckettomoda-location-seed-development-485000, GCP SAlocation-seed-loader@…(read-only), and the Workload Identity bindings to thelocation-seed-loaderK8s SA intomoda-dev/tomoda-prod. - Publisher IAM. The identity running
seed-publish.shmust bevar.location_seed_publisher(writer on the bucket). gcloudauthed againstdevelopment-485000, including ADC:gcloud auth login && gcloud auth application-default login && gcloud config set project development-485000.- A generated seed set. Produced by the app's
catalogseeder(app repo). The output directory holds NDJSON collections plus a checksummedmanifest.json. - The
tomoda-backendimage. The load Job runs the loader binary shipped inside it at/app/catalogseeder(validatethenload). See Image below.
1. Generate (app repo)¶
Run the app's catalogseeder to produce the seed set into a local directory. See the app repo's catalog-seeding docs for source selection and tuning. The output must contain manifest.json. The seed set is organized as:
seed/<version>/global/... planet-wide backbone (countries, cities, curated)
seed/<version>/local/... region-scoped supplements
seed/<version>/manifest.json
<version> is an operator-chosen tag: a date (2026-07-17) or a git sha. It names an immutable prefix in the bucket.
2. Publish¶
scripts/seed-publish.sh validates the local directory and uploads it under seed/<version>/:
./scripts/seed-publish.sh --version 2026-07-17 --dir ./seed-out
It refuses to publish when:
manifest.jsonis missing from the directory.catalogseeder validatefails (only when the binary is on PATH; skipped with a warning otherwise).seed/<version>/already exists, unless you pass--force. Prefer a fresh--versionover--forceso published versions stay immutable.
The bucket defaults to tomoda-location-seed-<project> from the active gcloud project; override with --bucket.
3. Load (on demand, dev first)¶
The load is a Kubernetes Job, not a CronJob. It is never scheduled and never auto-applied: the manifests under k8s/apps/tomoda/location-seed-load/ are a template that is not referenced by any Argo CD Application, so Argo leaves them alone. An operator renders and applies a run by hand.
The Job's initContainer pulls gs://<bucket>/seed/<version> into an emptyDir; the main container runs /app/catalogseeder validate --from /seed then /app/catalogseeder load --from /seed --city-highlights <N> against the env's Postgres, with DB creds from that env's backend-secrets-<env> + backend-config-<env>.
Two knobs per run: the seed version and city-highlights N (top-N backbone attractions eager-promoted per city; 0 skips the pass).
Render, set the knobs, apply¶
Build the dev overlay, substitute the version + N, and apply with a unique name (timestamp keeps successive runs distinct):
TS=$(date -u +%Y%m%d-%H%M%S)
SEED_VERSION=2026-07-17
CITY_HIGHLIGHTS=25
kubectl kustomize k8s/apps/tomoda/location-seed-load/overlays/dev \
| sed \
-e "s/REPLACE_ME_SEED_VERSION/${SEED_VERSION}/" \
-e "s/REPLACE_ME_CITY_HIGHLIGHTS/${CITY_HIGHLIGHTS}/" \
-e "s/name: location-seed-load$/name: location-seed-load-${TS}/" \
| kubectl apply -f -
This is the one sanctioned direct kubectl apply
The GitOps rule forbids kubectl apply for Argo-managed resources. This Job is deliberately outside Argo, so applying it by hand is the intended trigger, not a bypass. The manifests carry no schedule and no Argo Application.
Watch it:
kubectl -n tomoda-dev get job -l app=location-seed-load
kubectl -n tomoda-dev logs -f job/location-seed-load-${TS}
The Job cleans itself up 48h after finishing (ttlSecondsAfterFinished). backoffLimit: 0 means a failed load is inspected and re-run by hand rather than retried blindly.
4. Load prod¶
Repeat step 3 against the prod overlay once the dev load looks right. Prod targets tomoda_prod via postgres-prod and pins an immutable image tag rather than :latest:
TS=$(date -u +%Y%m%d-%H%M%S)
kubectl kustomize k8s/apps/tomoda/location-seed-load/overlays/prod \
| sed \
-e "s/REPLACE_ME_SEED_VERSION/${SEED_VERSION}/" \
-e "s/REPLACE_ME_CITY_HIGHLIGHTS/${CITY_HIGHLIGHTS}/" \
-e "s/name: location-seed-load$/name: location-seed-load-${TS}/" \
| kubectl apply -f -
Verify after a load¶
- Job succeeded:
kubectl -n tomoda-<env> get job -l app=location-seed-loadshowsCOMPLETIONS 1/1. - Logs: the loader prints per-collection
inserted/skipped/upgradedcounts and, when--city-highlights > 0, apromoted=line. - Row counts (read-only): connect via pgAdmin or
psqland checkSELECT count(*) FROM locations;andSELECT count(*) FROM countries;moved as expected. The load is checksum-gated per collection (seed_load_state), so re-running the same version is a near no-op.
Image¶
The loader ships inside the tomoda-backend image as a second binary at /app/catalogseeder (no dedicated image; backend/data is go:embed'd, so no data mount). Each overlay overrides the base placeholder IMAGE_PLACEHOLDER_CATALOGSEEDER to its env's …/tomoda-{dev,prod}-repo/tomoda-backend, and the Job command invokes /app/catalogseeder validate --from /seed then /app/catalogseeder load …. Dev tracks :latest; prod pins an immutable tag per run so a load is reproducible.
Deletion protection¶
The seed bucket, the two data buckets (tomoda-db-backups-*, tomoda-observability-*), and the CloudFront-signing secrets carry lifecycle { prevent_destroy = true } in Terraform. A terraform destroy or a resource removal that would delete one of them now fails the plan. Removing a protected resource is a two-step, deliberate act: delete the prevent_destroy block in one commit, then destroy in another. This guards the source-of-truth data (seed sets, backups, trace/log chunks) and the signing keys against an accidental destroy. See IAM for the loader Workload Identity binding and Secrets Management for the protected secrets.