| .. | ||
| argocd | ||
| docs | ||
| gitea | ||
| templates | ||
| Chart.yaml | ||
| README.md | ||
| values-prod.yaml | ||
| values.yaml | ||
planpalto-helm
Helm chart for the PlanPal stack. Converted from planpalto-infrato (kustomize)
one-to-one, targeting the fakhri-k8 EKS cluster: ESO reads secrets from AWS
Secrets Manager, and an ALB ingress serves the app.
Two ways to run it
- Direct Helm (below) - one release, no Argo. Good for a quick test.
- Argo CD App-of-Apps (argocd/) - Git-driven, two environments (staging + prod), self-healing. This is the deploy path. See Argo CD App-of-Apps.
Install (direct Helm)
helm upgrade --install planpal ./planpalto-helm
The chart creates the planpal namespace and the planpal-backend service
account itself. It does NOT install the controllers those objects depend on -
External Secrets Operator, the AWS Load Balancer Controller, and metrics-server
must already run on the cluster (they do on fakhri-k8).
Argo CD App-of-Apps
One root Application (argocd/root-app.yaml) syncs the child Applications in argocd/apps/: one per environment. Each child installs this same chart with a different value-file, so the two envs share nothing.
planpal-root (root App-of-Apps)
├─ planpal-staging ns planpal-staging host staging.fakhri-k8... own ALB
└─ planpal-prod ns planpal-prod host fakhri-k8... planpal-helm ALB
Objects inside each env are ordered by argocd.argoproj.io/sync-wave:
| wave | objects |
|---|---|
| 0 | ClusterSecretStore + ExternalSecrets, redis, nats, Services |
| 1 | app Deployments |
| 2 | HPA + PDB (need their Deployment to exist first) |
| 3 | ALB ingress |
| 4 | seed-admin Job |
Argo waits for a wave to be healthy before the next. So secrets and deps land before app pods, HPAs after the Deployments they target, ingress next, seed last. HPA and PDB sit one wave after the Deployments on purpose: an HPA applied before its target Deployment errors "scaleTargetRef not found" and fails the whole sync.
Chart source: in-cluster Gitea
Argo pulls the chart from Git, so the chart lives in a repo. This setup uses a
self-hosted Gitea on the cluster (gitea/gitea.yaml):
one pod, SQLite, one 5Gi PVC. Argo reaches it over the cluster network at
http://gitea-http.gitea.svc.cluster.local:3000, so no ingress or TLS is needed
for the Argo-to-Gitea hop.
Gitea is installed outside Argo (plain kubectl apply) because Argo pulls
its own manifests from Gitea - it cannot manage the git host it depends on.
Bootstrap order (once)
# 1. Git host
kubectl apply -f planpalto-helm/gitea/gitea.yaml
kubectl -n gitea rollout status deploy/gitea
# 2. Create the admin user + an org/repo in Gitea, then push this tree.
# (port-forward for the browser, or use the API)
kubectl -n gitea exec deploy/gitea -- \
gitea admin user create --admin --username planpal \
--password 'CHANGE_ME' --email you@example.com --must-change-password=false
kubectl -n gitea port-forward svc/gitea-http 3000:3000 # open http://localhost:3000, make org "planpal" + repo "ch4"
git init && git add . && git commit -m "init"
git remote add gitea http://planpal:CHANGE_ME@localhost:3000/planpal/ch4.git
git push gitea HEAD:main
# 3. Point Argo at it: the repoURL in the three argocd/*.yaml files is already
# http://gitea-http.gitea.svc.cluster.local:3000/planpal/ch4.git .
# Add the repo credentials to Argo, then apply the root:
argocd repo add http://gitea-http.gitea.svc.cluster.local:3000/planpal/ch4.git \
--username planpal --password CHANGE_ME
kubectl apply -f planpalto-helm/argocd/root-app.yaml
After that, git push to Gitea is the deploy. Argo notices, syncs, self-heals.
Change the
repoURLorg/repo (planpal/ch4) in all three argocd/ files if you name yours differently.
What it renders
| Group | Objects |
|---|---|
| Apps | Deployments backend, frontend, schedule/notification/ai workers; Services for backend+frontend; HPAs for all five; PDBs for backend+frontend |
| Deps | redis, nats (Deployment + Service each) |
| Secrets | 1 ClusterSecretStore + 4 ExternalSecrets (backend-env, aws-env, seed-env, frontend-env) |
| Ingress | ALB ingress on .Values.ingress.host with the ACM cert |
| Seed | one-shot seed-admin Job (TTL 300s) |
Multiple installs on one cluster
Each release is fully separated. namespace, the ALB group.name, and the
cluster-scoped ClusterSecretStore all default to the release name, so two
installs share nothing:
helm install planpal-a ./planpalto-helm --set ingress.host=a.example.com
helm install planpal-b ./planpalto-helm --set ingress.host=b.example.com
| Per-release resource | planpal-a | planpal-b |
|---|---|---|
| Namespace | planpal-a | planpal-b |
| ClusterSecretStore | planpal-a-aws-planpal | planpal-b-aws-planpal |
| ALB (group.name) | planpal-a | planpal-b |
Give each release a distinct ingress.host. Each release gets its own ALB, so
each also gets its own DNS name. Set the same ingress.groupName on two
releases only if you later want them to share one ALB.
Releases share the same AWS Secrets Manager keys (planpal/*) by default. Same
DB, admin seed, and config across releases, which you said is fine. To give a
release its own data, point externalSecrets.secrets at different keys and
allow the ESO controller role to read them.
external-dns
Set ingress.externalDNS=true to add the external-dns hostname annotation. The
external-dns controller then creates the Route53 record for ingress.host. This
needs external-dns installed on the cluster and permitted on the target hosted
zone. Without it, create the DNS record by hand (CNAME/alias to the ALB).
Common overrides
# different DNS name + cert
helm upgrade --install planpal ./planpalto-helm \
--set ingress.host=my.example.com \
--set ingress.certArn=arn:aws:acm:...
# bump the frontend image tag
helm upgrade --install planpal ./planpalto-helm \
--set image.frontend.tag=1.2
# skip the seed Job on a re-deploy
helm upgrade --install planpal ./planpalto-helm --set seedJob.enabled=false
Notes on the conversion
- The five app Deployments share one templated shape in
templates/apps.yaml, driven by theapps:map invalues.yaml. The map key is the workload name. An entry withimage: backendgets the service account and backend image; an entry withservice:gets a Service;hpa:/pdb:blocks render only when present. This is the kustomize base + per-app difference, expressed as data. - ESO is fully templated: add a
target: smKeypair underexternalSecrets.secretsand a new ExternalSecret appears. - The kustomize
secretGeneratorforplanpal-tlsand thebootstrap.yamlRDS-bootstrap Pod are NOT in this chart. Neither was in the kustomizeresources:list either. TLS now comes from ACM on the ALB, and RDS bootstrap is a one-shot manual step. Run it by hand if a fresh DB needs it.