# 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 | | 1 | app Deployments + Services + HPA + PDB | | 2 | ALB ingress | | 3 | seed-admin Job | Argo waits for a wave to be healthy before the next. So secrets and deps land before app pods, ingress after pods exist, seed last. ### 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. ```