ch4/planpalto-helm/README.md

165 lines
6.7 KiB
Markdown

# 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.
```