Skip to content

Deployment

Last Updated: 2026-08-11


Dashboard

Property Value
Namespace heezy
Deployment heezy-finance, 1 replica
Image 025066240222.dkr.ecr.us-east-2.amazonaws.com/heezy-finance:latest, imagePullPolicy: Always
Container port 80
Service heezy-finance ClusterIP :80, heezy-finance-nodeport NodePort 30860
Public URL finance.heezy.info via SWAG (192.168.1.25) and Cloudflare Access
Strategy Recreate
Resources requests 100m / 256Mi, limits 500m / 512Mi
Volume heezy-finance-config PVC (2Gi, Longhorn, RWO) at /data
Manifests heezy-k8s/apps/heezy-finance/

No node affinity and no nodeSelector. Both were removed after the 2026-06-20 nebula-5 outage. The pod schedules anywhere. The RWO Longhorn PVC is the reason the strategy is Recreate rather than RollingUpdate: two pods cannot attach the volume at once, so a rolling deploy would deadlock on a multi-attach error.

Environment

Database credentials come from the heezy-finance-postgres-credentials secret, projected by an ExternalSecret from OpenBao production/heezy/postgres/heezy-credentials. DB_HOST is bigboi.heezy.local, resolved by dnsmasq.

The deployment also carries envFrom: heezy-finance-gmail even though the web app makes no Gmail calls. That is deliberate: running amazon_orders.py by hand inside the pod then picks up the same OpenBao-backed credential the CronJob uses, instead of the stale file that used to be mounted at /credentials. The file fallback is gone and a test asserts it stays gone.


Scheduled Jobs

All three run the same heezy-finance:latest image with a different command, all with concurrencyPolicy: Forbid.

CronJob Schedule Command Purpose
heezy-finance-sync 0 * * * * amazon_orders.py --hours 2 Gmail order ingest
heezy-statement-scanner 30 * * * * scan_statements.py Parses PDFs from /ingest/new on the nfs-heezy-ingest PVC
heezy-budget-alerts 0 14 * * * budget_alerts.py Posts budget warnings to Discord

The scanner is offset to :30 so it does not contend with the sync at :00.

heezy-finance-sync carries a podAffinity requiring the same node as the heezy-finance pod. The other two set affinity: {} to explicitly clear inherited affinity and run anywhere.

heezy-budget-alerts reads DISCORD_WEBHOOK_URL from the heezy-finance-discord secret, marked optional: true, so a missing webhook degrades to a no-op rather than a crash loop.


Deployment Purpose
statements Statement upload web UI at statements.heezy.info. Writes files to the NFS ingest path, does no parsing. NodePort 30851
receipts Receipt OCR at receipts.heezy.info. NodePort 30850

Deploy Pipeline

edit dockerfiles/heezy-finance/*
  → git push gitea main (heezy-containers)
  → .gitea/workflows/test-heezy-finance.yml   pytest + coverage gate (floor 13)
  → .gitea/workflows/build.yml                builds and pushes :latest to ECR
  → .gitea/workflows/deploy-heezy-finance.yml clones heezy-k8s, commits .deploy-trigger
  → heezy-k8s/auto-deploy.yaml                kubectl apply -k, rollout restart, rollout status

Push to Gitea, not GitHub. GitHub is a read-only mirror and triggers nothing.

Because :latest is a moving tag, the deploy step relies on rollout restart to force a re-pull. The three CronJobs pick up the new image on their next scheduled run, no restart needed.

See CI/CD for the full pipeline.


Health Checks

kubectl get pods -n heezy -l app=heezy-finance
kubectl logs -n heezy -l app=heezy-finance --tail=50
kubectl get cronjob -n heezy | grep heezy
curl -s http://192.168.1.15:30860/api/latest-data-period

There are no liveness or readiness probes on the deployment. A pod that starts but fails to reach Postgres reports Running and returns 500s.


Open Infrastructure Items

Item Notes
No liveness or readiness probe A broken DB connection looks healthy to k8s
PVC is RWO Recreate strategy means a few seconds of downtime on every deploy. RWX or dropping the PVC entirely would remove it
PVC necessity unaudited /data is mounted but the app's state lives in Postgres. Worth checking whether the volume is needed at all