ch4/planpalto-helm
2026-09-02 16:10:27 +07:00
..
argocd grafana: move to fakhri-grafana.private.sandbox... + new cert 2026-09-02 15:56:19 +07:00
dashboards monitoring: scrape prod app metrics + Grafana PlanPal Prod dashboard 2026-09-02 15:35:29 +07:00
docs planpalto-helm: App-of-Apps (staging+prod) + gitea 2026-09-02 13:45:57 +07:00
gitea planpalto-helm: App-of-Apps (staging+prod) + gitea 2026-09-02 13:45:57 +07:00
templates monitoring: scrape prod app metrics + Grafana PlanPal Prod dashboard 2026-09-02 15:35:29 +07:00
Chart.yaml planpalto-helm: App-of-Apps (staging+prod) + gitea 2026-09-02 13:45:57 +07:00
README.md sync-waves: HPA/PDB after Deployments (fix scaleTargetRef-not-found) 2026-09-02 13:51:31 +07:00
values-prod.yaml prod backend: 250m/500m + HPA max 12 to spread across nodes 2026-09-02 16:10:27 +07:00
values.yaml monitoring: workers serve no metrics; scrape backend only 2026-09-02 15:38:09 +07:00

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

  1. Direct Helm (below) - one release, no Argo. Good for a quick test.
  2. 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 repoURL org/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 the apps: map in values.yaml. The map key is the workload name. An entry with image: backend gets the service account and backend image; an entry with service: 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: smKey pair under externalSecrets.secrets and a new ExternalSecret appears.
  • The kustomize secretGenerator for planpal-tls and the bootstrap.yaml RDS-bootstrap Pod are NOT in this chart. Neither was in the kustomize resources: 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.