Run the ERA5 lake service in every environment (#20)
All checks were successful
secrets-guard / encrypted (push) Successful in 6s

This commit is contained in:
emi 2026-07-23 22:32:13 +00:00
parent 21eea3a4c2
commit 8e09155aaf
6 changed files with 61 additions and 6 deletions

View file

@ -70,4 +70,10 @@ jobs:
# thermograph-lan is host-native (the runner job runs directly on the
# LAN box, not over SSH like beta/prod), so plain env vars on the
# command reach deploy-dev.sh directly -- no ssh-action needed here.
# The lake creds ride the Actions S3 secrets: dev renders no vault
# (deploy-dev.sh's no-op secrets path), and compose interpolates
# ${THERMOGRAPH_LAKE_S3_*} straight from the deploy environment.
env:
THERMOGRAPH_LAKE_S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
THERMOGRAPH_LAKE_S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
run: SERVICE=backend BACKEND_IMAGE_TAG=${{ steps.tag.outputs.tag }} bash "$HOME/thermograph-dev/infra/deploy/deploy-dev.sh"

View file

@ -177,11 +177,12 @@ esac
export BACKEND_IMAGE_TAG="${BACKEND_IMAGE_TAG:-local}"
export FRONTEND_IMAGE_TAG="${FRONTEND_IMAGE_TAG:-local}"
# Which compose services this run pulls/rolls.
# Which compose services this run pulls/rolls. lake runs the backend image
# with a different role, so it rolls with every backend deploy.
case "$SERVICE" in
backend) TARGETS=(backend) ;;
backend) TARGETS=(backend lake) ;;
frontend) TARGETS=(frontend) ;;
all) TARGETS=(backend frontend) ;;
all) TARGETS=(backend lake frontend) ;;
esac
# Login only when a token is supplied. The SSH/CI deploy paths (deploy.yml,

View file

@ -16,6 +16,8 @@ THERMOGRAPH_VAPID_PRIVATE_KEY: ENC[AES256_GCM,data:sTtkAbMVUtwvRssCZJa2PWUjlfnYS
THERMOGRAPH_VAPID_PUBLIC_KEY: ENC[AES256_GCM,data:SnFw+pwNgZlxMEhMUH/1daMZlRB2QFNJHtL+4C3S1eFcJJdT2uNGkY7Ym9FPfx1V7cwt/3urHg1oVgdDzYaIal2wye4l5HhKBP5PjtQkDW+Pck24kxul,iv:Ku4MhsrAfyA2Y3CqhGIsYKrjhKQ4cqAYjCqjFeEvBWE=,tag:hw42iS+79D7gNf+BPTYM0g==,type:str]
THERMOGRAPH_VAPID_CONTACT: ENC[AES256_GCM,data:ox+hHiFMTPZqYwICDSwPwcT13ZDSKiGcAVHfVVMNYQ==,iv:DssV/olPXERpdmXNMmzjVYvhn/0zmuSp2XlTs42pIi0=,tag:4YHXI1BFbGZ6JsdXqJuf3w==,type:str]
REGISTRY_TOKEN: ENC[AES256_GCM,data:pC1Zzs56la8qjzWcbkuKyeVHwS5WfIe+mrLm5Sdi/LKYziAvRU5cOA==,iv:57PlJEBdgwNJsfFdNOGqAnKk8l6zpmMSWYFEKS6pn+4=,tag:pPftNHUVU6+CY2nWMaXO6w==,type:str]
THERMOGRAPH_LAKE_S3_ACCESS_KEY: ENC[AES256_GCM,data:ZEQ278P7azMKlYMjUraP6hIqnjymqL90zEbvTNC9iXU=,iv:5nXXZwx7RfqwCMqx5LWtxQy2nQcq+6Yr79ulBi6BOnE=,tag:jXcCCCcz4NsDoIZwa/ji4A==,type:str]
THERMOGRAPH_LAKE_S3_SECRET_KEY: ENC[AES256_GCM,data:p5qCrHWoH1i3smDZZBIO8zMSWWxDARYsTY12AA0E8ao=,iv:PVOEQ0kBFeYaE/FT2UswtSTY7S84U/YOPSbcsXR51f4=,tag:JFwaOwAYInPs199li66U/g==,type:str]
sops:
age:
- enc: |
@ -27,7 +29,7 @@ sops:
0+PQdtDxom72BqrAoj5lhepxW8YWPqQIRXsVS/XYQEQfPJuuuiRMKw==
-----END AGE ENCRYPTED FILE-----
recipient: age1xx4dzs0dxlwvkv9sjuqzsphl7lfrxannkfken374yu2qvvcte9sqzktqt2
lastmodified: "2026-07-22T03:17:28Z"
mac: ENC[AES256_GCM,data:npF+ngF4Khj3NJ2PNFcQjQYIrZVpdPM0drk43pAI6HfFaynX8JOqRFZRsH0sCfMoPkXB/ffCJxla1Tt3Mu3zgKTNSWcn+mjzl+dYvlruY1VylHLS+GnTO9zKeQcwbrYzTfZNWzQhm6/mw8KCL3791U/xNHwJc6g5VxRUWl6TBdo=,iv:JLzY8rm33NLG8CqyAlU6vBhlODUiRipKu9daoRvgyzg=,tag:yIsxVZAnw8FD7Bt/x55wMQ==,type:str]
lastmodified: "2026-07-23T22:30:10Z"
mac: ENC[AES256_GCM,data:JWfkQk4Z1ohPha9LID/hvZOycmDy0sUyjiqDdCidfCQEsPx+OwhPatcM31x1ZKFgh3TyYTxrauoSTlIeuwfy7+R9jATDdBBZe9zMqgs6Szu9WUdFiaZzIFpG7SEVw+8IqIyAuqZVSW3k5yHmchcmkNyDekV+1RIF8wxUrOeSCVQ=,iv:6PXUiCnYBiFyNPnCRAs6f7a+RuvWI4jSIMXwuMn7WjA=,tag:y+Ix2m65hQRkSt8FylfKSg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.2

View file

@ -157,9 +157,16 @@ if [ "$SERVICE" = "all" ] || ! docker service inspect "${STACK_NAME}_web" >/dev/
else
case "$SERVICE" in
backend)
echo "==> Rolling web + worker to $BACKEND_IMAGE"
echo "==> Rolling web + worker + lake to $BACKEND_IMAGE"
docker service update --with-registry-auth --detach=false --image "$BACKEND_IMAGE" "${STACK_NAME}_web"
docker service update --with-registry-auth --detach=false --image "$BACKEND_IMAGE" "${STACK_NAME}_worker"
# lake ships in the same image; a stack file predating it has no service
# yet — the next SERVICE=all stack deploy creates it, so don't fail here.
if docker service inspect "${STACK_NAME}_lake" >/dev/null 2>&1; then
docker service update --with-registry-auth --detach=false --image "$BACKEND_IMAGE" "${STACK_NAME}_lake"
else
echo " (no ${STACK_NAME}_lake service yet; created on the next full stack deploy)"
fi
;;
frontend)
echo "==> Rolling frontend to $FRONTEND_IMAGE"

View file

@ -40,6 +40,9 @@ services:
db:
cpus: !reset null
deploy: !reset null
lake:
cpus: !reset null
deploy: !reset null
# Uncapped memory on dev too (prod ceilings it at 8g). The Postgres/DuckDB
# memory *budget* is still the ~8 GB derived by deploy/db/init/20-tuning.sh from
# the default DB_MEMORY (raise DB_MEMORY to give dev more); shm_size stays (it's

View file

@ -124,6 +124,11 @@ services:
# (NOT /app/data) so it never shadows the Python `data/` package -- see the
# volumes: note below and thermograph-backend paths.py.
THERMOGRAPH_SINGLETON_LOCK: /state/notifier.lock
# History reads ask the lake service before any third-party source; if
# the lake has no bucket creds (LAN without them exported) it answers
# 503 and climate.py falls straight through to NASA — an accelerator,
# never a dependency.
THERMOGRAPH_LAKE_URL: http://lake:8141
# Prod secrets live in /etc/thermograph.env: POSTGRES_PASSWORD,
# THERMOGRAPH_AUTH_SECRET, THERMOGRAPH_VAPID_PRIVATE_KEY/_PUBLIC_KEY,
# THERMOGRAPH_COOKIE_SECURE=1, mail/Discord keys, ... (see
@ -157,6 +162,36 @@ services:
- "127.0.0.1:8137:8137"
restart: unless-stopped
# The ERA5 lake service: partition-pruned reads over the era5-thermograph
# bucket with a local parquet cache, same backend image with
# THERMOGRAPH_ROLE=lake (the entrypoint starts lake_app — no database, no
# migrations, so no depends_on: db). Bucket creds come from
# /etc/thermograph.env where the vault renders them (beta/prod) or from the
# deploy environment (LAN — deploy-dev.yml injects the Actions S3 secrets);
# without them the service stays healthy and every read falls through to
# NASA. Never published on a host port: only the backend talks to it.
lake:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
environment:
THERMOGRAPH_ROLE: lake
PORT: 8141
THERMOGRAPH_SERVICE_ROLE: backend
WORKERS: "1"
THERMOGRAPH_LAKE_CACHE: /state/lake-cache
THERMOGRAPH_LAKE_S3_ACCESS_KEY: ${THERMOGRAPH_LAKE_S3_ACCESS_KEY:-}
THERMOGRAPH_LAKE_S3_SECRET_KEY: ${THERMOGRAPH_LAKE_S3_SECRET_KEY:-}
env_file:
- path: /etc/thermograph.env
required: false
volumes:
- lakecache:/state
cpus: ${LAKE_CPUS:-2}
deploy:
resources:
limits:
cpus: "${LAKE_CPUS:-2}"
restart: unless-stopped
frontend:
# Frontend's OWN image, published by thermograph-frontend's build-push.yml
# from its own Dockerfile (which starts `uvicorn app:app` directly -- no
@ -187,6 +222,7 @@ volumes:
pgdata: {}
appdata: {}
applogs: {}
lakecache: {}
networks:
# Pin the default network's subnet + gateway so the host Postfix can rely on a