# 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/](argocd/)) - Git-driven, two environments (staging + prod), self-healing. This is the deploy path. See [Argo CD App-of-Apps](#argo-cd-app-of-apps). ## Install (direct Helm) ```bash 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](argocd/root-app.yaml)) syncs the child Applications in [argocd/apps/](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](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) ```bash # 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/](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: ```bash 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 ```bash # 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. ```