planpalto-helm: App-of-Apps (staging+prod) + gitea
This commit is contained in:
commit
0e877eac63
6
planpalto-helm/Chart.yaml
Normal file
6
planpalto-helm/Chart.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: planpal
|
||||||
|
description: PlanPal app stack for the fakhri-k8 EKS cluster (ESO + ALB ingress).
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "1.0"
|
||||||
164
planpalto-helm/README.md
Normal file
164
planpalto-helm/README.md
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# 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.
|
||||||
|
```
|
||||||
29
planpalto-helm/argocd/apps/planpal-prod.yaml
Normal file
29
planpalto-helm/argocd/apps/planpal-prod.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: planpal-prod
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/planpal/ch4.git
|
||||||
|
targetRevision: HEAD
|
||||||
|
path: planpalto-helm
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- values.yaml
|
||||||
|
- values-prod.yaml
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: planpal-prod
|
||||||
|
syncPolicy:
|
||||||
|
# ponytail: prod stays automated to match staging. Drop `automated` here and
|
||||||
|
# sync prod by hand from the Argo UI if you want a manual prod gate.
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ServerSideApply=true
|
||||||
29
planpalto-helm/argocd/apps/planpal-staging.yaml
Normal file
29
planpalto-helm/argocd/apps/planpal-staging.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: planpal-staging
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/planpal/ch4.git
|
||||||
|
targetRevision: HEAD
|
||||||
|
path: planpalto-helm
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- values.yaml
|
||||||
|
- values-staging.yaml
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: planpal-staging
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
# ClusterSecretStore is cluster-scoped; without this Argo warns about the
|
||||||
|
# missing namespace on a cluster-scoped resource. Harmless, silences it.
|
||||||
|
- ServerSideApply=true
|
||||||
28
planpalto-helm/argocd/root-app.yaml
Normal file
28
planpalto-helm/argocd/root-app.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Root App-of-Apps. Its only job is to sync the child Applications in
|
||||||
|
# argocd/apps/ (one per environment). Apply this ONCE by hand; Argo owns the
|
||||||
|
# rest. Bootstrap: kubectl apply -f planpalto-helm/argocd/root-app.yaml
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: planpal-root
|
||||||
|
namespace: argocd
|
||||||
|
# keep the root around if you ever `kubectl delete` it by accident
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
# In-cluster Gitea. Argo talks to Gitea over the cluster network, so no
|
||||||
|
# ingress or TLS is needed for this hop. Change org/repo to match your push.
|
||||||
|
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/planpal/ch4.git
|
||||||
|
targetRevision: HEAD
|
||||||
|
path: planpalto-helm/argocd/apps
|
||||||
|
directory:
|
||||||
|
recurse: true
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: argocd
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
467
planpalto-helm/docs/render-pipeline.html
Normal file
467
planpalto-helm/docs/render-pipeline.html
Normal file
@ -0,0 +1,467 @@
|
|||||||
|
<title>planpalto-helm — render pipeline</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--paper: #E7EAEE;
|
||||||
|
--paper-2: #DDE1E7;
|
||||||
|
--rule: #A4ADB8;
|
||||||
|
--ink: #242A31;
|
||||||
|
--ink-dim: #5C6672;
|
||||||
|
--accent: #1F5FA8;
|
||||||
|
--accent-weak: rgba(31, 95, 168, 0.14);
|
||||||
|
--grid: rgba(164, 173, 184, 0.14);
|
||||||
|
|
||||||
|
--font-display: "IBM Plex Mono", ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Consolas, monospace;
|
||||||
|
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Consolas, monospace;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--paper: #161A1F; --paper-2: #1D232A; --rule: #39424D;
|
||||||
|
--ink: #D6DBE1; --ink-dim: #828C98; --accent: #4C93E0;
|
||||||
|
--accent-weak: rgba(76, 147, 224, 0.18);
|
||||||
|
--grid: rgba(57, 66, 77, 0.22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--paper: #161A1F; --paper-2: #1D232A; --rule: #39424D;
|
||||||
|
--ink: #D6DBE1; --ink-dim: #828C98; --accent: #4C93E0;
|
||||||
|
--accent-weak: rgba(76, 147, 224, 0.18);
|
||||||
|
--grid: rgba(57, 66, 77, 0.22);
|
||||||
|
}
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--paper: #E7EAEE; --paper-2: #DDE1E7; --rule: #A4ADB8;
|
||||||
|
--ink: #242A31; --ink-dim: #5C6672; --accent: #1F5FA8;
|
||||||
|
--accent-weak: rgba(31, 95, 168, 0.14);
|
||||||
|
--grid: rgba(164, 173, 184, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; border-radius: 0; }
|
||||||
|
body {
|
||||||
|
margin: 0; padding: 20px 12px;
|
||||||
|
background: var(--paper); color: var(--ink);
|
||||||
|
font-family: var(--font-body); font-size: 14px; line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Double-rule drafting frame */
|
||||||
|
.frame-outer { max-width: 1100px; margin: 0 auto; border: 2px solid var(--ink); padding: 6px; }
|
||||||
|
.frame-inner {
|
||||||
|
border: 1px solid var(--ink); padding: 24px;
|
||||||
|
background-color: var(--paper);
|
||||||
|
background-image:
|
||||||
|
repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px 24px),
|
||||||
|
repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono { font-family: var(--font-mono); }
|
||||||
|
.disp { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; }
|
||||||
|
.dim { color: var(--ink-dim); }
|
||||||
|
.acc { color: var(--accent); }
|
||||||
|
.ms { font-family: var(--font-mono); font-size: 11px; line-height: 1.35; }
|
||||||
|
|
||||||
|
.sep { border: 0; border-top: 1px solid var(--rule); margin: 20px 0; }
|
||||||
|
|
||||||
|
/* ── (a) title / context strip ── */
|
||||||
|
.strip { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; flex-wrap: wrap; }
|
||||||
|
.strip h1 { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; font-size: 15px; margin: 0 0 4px; text-wrap: balance; }
|
||||||
|
.strip .sub { font-size: 14px; color: var(--ink-dim); max-width: 60ch; margin: 0; }
|
||||||
|
.legend { display: flex; gap: 18px; flex-wrap: wrap; align-items: center; }
|
||||||
|
.legend .item { display: flex; align-items: center; gap: 6px; }
|
||||||
|
.sw-acc { width: 22px; height: 0; border-top: 2px solid var(--accent); }
|
||||||
|
.sw-ink { width: 22px; height: 0; border-top: 1px solid var(--ink); }
|
||||||
|
.stamp { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border: 1px solid var(--ink); font-family: var(--font-display); font-size: 11px; flex: none; }
|
||||||
|
|
||||||
|
.secthead { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; font-size: 12px; margin: 0 0 12px; }
|
||||||
|
.secthead .st-sub { font-family: var(--font-body); text-transform: none; letter-spacing: 0; color: var(--ink-dim); font-size: 12px; margin-left: 10px; }
|
||||||
|
|
||||||
|
/* ── (b→h) pipeline spine ── */
|
||||||
|
.spine { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0; position: relative; }
|
||||||
|
.stage { padding: 0 12px 0 0; position: relative; }
|
||||||
|
.stage:not(:last-child)::after {
|
||||||
|
/* dimension arrow between stages */
|
||||||
|
content: ""; position: absolute; top: 9px; right: 4px; width: 20px; height: 0;
|
||||||
|
border-top: 1.5px solid var(--accent);
|
||||||
|
}
|
||||||
|
.stage:not(:last-child) .flow-note {
|
||||||
|
position: absolute; top: -6px; right: 2px; color: var(--accent); font-size: 10px;
|
||||||
|
}
|
||||||
|
.stage-hd { display: flex; gap: 8px; align-items: flex-start; }
|
||||||
|
.stage-stamp { width: 20px; height: 20px; border: 1px solid var(--ink); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: 11px; flex: none; }
|
||||||
|
.stage-name { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; }
|
||||||
|
.stage-desc { font-size: 11px; color: var(--ink-dim); margin-top: 6px; padding-left: 0; line-height: 1.4; }
|
||||||
|
.stage-desc code { font-family: var(--font-mono); color: var(--ink); }
|
||||||
|
|
||||||
|
/* recessed panels / wells */
|
||||||
|
.well { background: var(--paper-2); border: 1px solid var(--rule); border-top: 1px solid var(--ink); padding: 14px; }
|
||||||
|
.well-hd { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.08em; font-size: 11px; margin: 0 0 10px; }
|
||||||
|
|
||||||
|
.subgrid { display: grid; grid-template-columns: 1.1fr 1fr; gap: 16px; margin-top: 14px; }
|
||||||
|
|
||||||
|
.filetree { font-family: var(--font-mono); font-size: 12px; line-height: 1.55; white-space: pre; }
|
||||||
|
.filetree .an { color: var(--ink-dim); }
|
||||||
|
|
||||||
|
/* command callout */
|
||||||
|
.cmd-wrap { margin-top: 14px; }
|
||||||
|
.cmd-lead { color: var(--accent); font-size: 10px; font-family: var(--font-mono); margin-bottom: 4px; }
|
||||||
|
.cmd-box { position: relative; border: 1px solid var(--ink); background: var(--paper); padding: 10px 12px; font-family: var(--font-mono); font-size: 13px; overflow-x: auto; }
|
||||||
|
.cmd-box::before { content: ""; position: absolute; left: -8px; top: 50%; width: 8px; height: 0; border-top: 1px solid var(--accent); }
|
||||||
|
.cmd-note { font-family: var(--font-mono); font-size: 11px; color: var(--ink-dim); margin-top: 6px; }
|
||||||
|
|
||||||
|
/* render well 3 columns */
|
||||||
|
.render3 { display: grid; grid-template-columns: 1fr 1.15fr 1fr; gap: 0; }
|
||||||
|
.render3 .rcol { padding: 0 16px; border-left: 1px solid var(--rule); position: relative; }
|
||||||
|
.render3 .rcol:first-child { border-left: 0; padding-left: 0; }
|
||||||
|
.render3 .rcol:last-child { padding-right: 0; }
|
||||||
|
.render3 .rcol::after { content: attr(data-cross); position: absolute; top: 24px; right: -6px; z-index: 2; color: var(--accent); font-size: 9px; font-family: var(--font-mono); background: var(--paper-2); padding: 0 2px; }
|
||||||
|
.render3 .rcol:last-child::after { content: none; }
|
||||||
|
.rcol .rhd { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; margin: 0 0 8px; }
|
||||||
|
.rcol pre { font-family: var(--font-mono); font-size: 11.5px; line-height: 1.5; margin: 0; white-space: pre-wrap; word-break: break-word; }
|
||||||
|
.mech { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.mech li { font-family: var(--font-mono); font-size: 11.5px; line-height: 1.5; margin-bottom: 6px; }
|
||||||
|
.mech .tok { color: var(--ink); }
|
||||||
|
.mech .gl { color: var(--ink-dim); }
|
||||||
|
|
||||||
|
.twin { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 14px; }
|
||||||
|
.chain { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 11.5px; }
|
||||||
|
.chain .bx { border: 1px solid var(--rule); padding: 3px 7px; }
|
||||||
|
.chain .ar { color: var(--accent); }
|
||||||
|
.addon-row { display: grid; grid-template-columns: auto auto 1fr; gap: 8px; align-items: baseline; font-family: var(--font-mono); font-size: 11.5px; padding: 5px 0; border-top: 1px solid var(--rule); }
|
||||||
|
.addon-row:first-of-type { border-top: 0; }
|
||||||
|
.addon-row .ar { color: var(--accent); }
|
||||||
|
.addon-row .obj { color: var(--ink); }
|
||||||
|
.addon-row .gl { color: var(--ink-dim); }
|
||||||
|
|
||||||
|
/* ── (e) BOM table ── */
|
||||||
|
.bom { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 13px; }
|
||||||
|
.bom th, .bom td { text-align: left; padding: 7px 10px; border: 1px solid var(--rule); vertical-align: top; }
|
||||||
|
.bom thead th { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; color: var(--ink-dim); background: var(--paper-2); }
|
||||||
|
.bom tbody tr:nth-child(even) td { background: var(--paper-2); }
|
||||||
|
.bom .note { color: var(--accent); font-size: 11px; }
|
||||||
|
.bom .note-dim { color: var(--ink-dim); font-size: 11px; }
|
||||||
|
.bom .sum td { background: var(--accent-weak); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); color: var(--accent); font-size: 12.5px; }
|
||||||
|
.bom-wrap { overflow-x: auto; }
|
||||||
|
|
||||||
|
/* ── lower band ── */
|
||||||
|
.lower { display: grid; grid-template-columns: 62fr 38fr; gap: 24px; align-items: start; }
|
||||||
|
.detail { border: 1px solid var(--ink); padding: 16px 16px 14px; position: relative; }
|
||||||
|
.detail-tab { position: absolute; top: -1px; left: -1px; border: 1px solid var(--ink); background: var(--paper); font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.08em; font-size: 11px; padding: 3px 8px; }
|
||||||
|
.detail-cap { font-size: 11.5px; color: var(--ink-dim); margin: 22px 0 16px; line-height: 1.45; }
|
||||||
|
.detail-cap code { font-family: var(--font-mono); color: var(--ink); }
|
||||||
|
|
||||||
|
/* DETAIL A fan */
|
||||||
|
.fan { position: relative; }
|
||||||
|
.fan svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
|
||||||
|
.fan-grid { display: grid; grid-template-columns: minmax(150px,1.1fr) 78px minmax(210px,1.5fr); gap: 0; align-items: stretch; }
|
||||||
|
.lanes { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.srcbox { border: 1px solid var(--rule); padding: 6px 8px; min-height: 44px; display: flex; flex-direction: column; justify-content: center; }
|
||||||
|
.srcbox .nm { font-family: var(--font-mono); font-size: 12.5px; }
|
||||||
|
.srcbox .tags { margin-top: 3px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.srcbox .tag { font-family: var(--font-mono); font-size: 10.5px; color: var(--accent); }
|
||||||
|
.bracket-col { display: flex; align-items: center; justify-content: center; position: relative; }
|
||||||
|
.bracket-lbl { font-family: var(--font-mono); font-size: 10px; color: var(--accent); writing-mode: vertical-rl; text-orientation: mixed; letter-spacing: 0.04em; }
|
||||||
|
.outlanes { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.oline { min-height: 44px; display: flex; align-items: center; gap: 5px; flex-wrap: nowrap; }
|
||||||
|
.objbox { border: 1px solid var(--rule); padding: 4px 7px; font-family: var(--font-mono); font-size: 11.5px; white-space: nowrap; }
|
||||||
|
.objbox.dep { border-color: var(--ink); }
|
||||||
|
.gate { font-family: var(--font-mono); font-size: 10px; color: var(--ink-dim); white-space: nowrap; }
|
||||||
|
.gate::before { content: "├ "; color: var(--accent); }
|
||||||
|
.fan-recon { font-family: var(--font-mono); font-size: 11px; color: var(--ink-dim); margin-top: 14px; line-height: 1.45; }
|
||||||
|
.fan-recon code { color: var(--ink); }
|
||||||
|
.fan-x5 { position: absolute; font-family: var(--font-mono); font-size: 10px; color: var(--accent); }
|
||||||
|
|
||||||
|
/* DETAIL B multi-release */
|
||||||
|
.rel-cols { display: flex; gap: 48px; justify-content: flex-start; }
|
||||||
|
.rel-col { flex: 1; min-width: 0; }
|
||||||
|
.rel-title { font-family: var(--font-mono); font-size: 12px; color: var(--accent); text-align: center; margin-bottom: 6px; }
|
||||||
|
.rel-stack { border: 1px solid var(--rule); }
|
||||||
|
.rel-cell { border-top: 1px solid var(--rule); padding: 7px 9px; font-family: var(--font-mono); font-size: 11.5px; line-height: 1.4; }
|
||||||
|
.rel-cell:first-child { border-top: 0; }
|
||||||
|
.rel-cell .v { color: var(--accent); }
|
||||||
|
.rel-note { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-dim); margin-top: 14px; line-height: 1.55; white-space: pre-wrap; }
|
||||||
|
.rel-note .v { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── title block ── */
|
||||||
|
.tblock-wrap { display: flex; justify-content: flex-end; margin-top: 20px; }
|
||||||
|
.tblock { width: 340px; border: 1px solid var(--ink); }
|
||||||
|
.tblock .tb-row { display: grid; grid-template-columns: 128px 1fr; border-top: 1px solid var(--rule); }
|
||||||
|
.tblock .tb-row:first-child { border-top: 0; }
|
||||||
|
.tblock .tb-k { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.06em; font-size: 10px; color: var(--ink-dim); padding: 5px 8px; border-right: 1px solid var(--rule); }
|
||||||
|
.tblock .tb-v { font-family: var(--font-mono); font-size: 11px; padding: 5px 8px; }
|
||||||
|
.tblock .tb-v.v-acc { color: var(--accent); }
|
||||||
|
|
||||||
|
@media (max-width: 959px) {
|
||||||
|
.spine { grid-template-columns: repeat(3, 1fr); row-gap: 20px; }
|
||||||
|
.stage:nth-child(3)::after { content: none; }
|
||||||
|
.render3 { grid-template-columns: 1fr; }
|
||||||
|
.render3 .rcol { border-left: 0; padding: 12px 0 0; border-top: 1px solid var(--rule); }
|
||||||
|
.render3 .rcol:first-child { border-top: 0; padding-top: 0; }
|
||||||
|
.render3 .rcol::after { content: none; }
|
||||||
|
}
|
||||||
|
@media (max-width: 719px) {
|
||||||
|
.strip { flex-direction: column; }
|
||||||
|
.spine { grid-template-columns: 1fr; }
|
||||||
|
.stage::after { content: none !important; }
|
||||||
|
.stage { border-top: 1px solid var(--rule); padding-top: 12px; }
|
||||||
|
.stage:first-child { border-top: 0; padding-top: 0; }
|
||||||
|
.subgrid, .twin, .lower { grid-template-columns: 1fr; }
|
||||||
|
.fan-grid { grid-template-columns: 1fr; gap: 10px; }
|
||||||
|
.fan svg { display: none; }
|
||||||
|
.bracket-col { justify-content: flex-start; }
|
||||||
|
.bracket-lbl { writing-mode: horizontal-tb; }
|
||||||
|
.tblock { width: 100%; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.draw { stroke-dasharray: var(--len); stroke-dashoffset: var(--len); animation: draw 0.9s ease-out forwards; }
|
||||||
|
.fan .draw { animation-duration: 0.7s; }
|
||||||
|
@keyframes draw { to { stroke-dashoffset: 0; } }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="frame-outer"><div class="frame-inner">
|
||||||
|
|
||||||
|
<!-- (a) title / context strip -->
|
||||||
|
<div class="strip">
|
||||||
|
<div>
|
||||||
|
<h1>planpalto-helm — render pipeline</h1>
|
||||||
|
<p class="sub">How <span class="mono">helm install</span> turns the chart on disk into running objects on the fakhri-k8 EKS cluster.</p>
|
||||||
|
</div>
|
||||||
|
<div class="legend">
|
||||||
|
<div class="item"><span class="sw-acc"></span><span class="ms dim">live / rendered value</span></div>
|
||||||
|
<div class="item"><span class="sw-ink"></span><span class="ms dim">static linework</span></div>
|
||||||
|
<div class="item"><span class="stamp">N</span><span class="ms dim">pipeline stage</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="sep">
|
||||||
|
|
||||||
|
<!-- (b→h) pipeline spine -->
|
||||||
|
<p class="secthead">Render pipeline — 6 stages</p>
|
||||||
|
<div class="spine">
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-hd"><span class="stage-stamp">1</span><span class="stage-name">Inputs</span></div>
|
||||||
|
<p class="stage-desc"><code>Chart.yaml</code> · <code>values.yaml</code> · <code>templates/*</code> · <code>_helpers.tpl</code> · optional <code>-f envs/a.yaml</code></p>
|
||||||
|
<span class="flow-note">paths</span>
|
||||||
|
</div>
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-hd"><span class="stage-stamp">2</span><span class="stage-name">Command</span></div>
|
||||||
|
<p class="stage-desc">You run one <code>helm</code> command. Helm reads the chart and any override file.</p>
|
||||||
|
<span class="flow-note">flags+values</span>
|
||||||
|
</div>
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-hd"><span class="stage-stamp">3</span><span class="stage-name">Render</span></div>
|
||||||
|
<p class="stage-desc">Go templating merges values + <code>.Release.*</code>, walks <code>templates/</code>, emits one multi-doc YAML.</p>
|
||||||
|
<span class="flow-note">YAML</span>
|
||||||
|
</div>
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-hd"><span class="stage-stamp">4</span><span class="stage-name">Submit</span></div>
|
||||||
|
<p class="stage-desc">Manifest sent to the k8s API server. Helm records the revision in a Secret.</p>
|
||||||
|
<span class="flow-note">objects</span>
|
||||||
|
</div>
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-hd"><span class="stage-stamp">5</span><span class="stage-name">Reconcile</span></div>
|
||||||
|
<p class="stage-desc">Controllers act: Deployment → ReplicaSet → Pod. Scheduler places Pods on EKS nodes.</p>
|
||||||
|
<span class="flow-note">pods</span>
|
||||||
|
</div>
|
||||||
|
<div class="stage">
|
||||||
|
<div class="stage-hd"><span class="stage-stamp">6</span><span class="stage-name">Add-ons</span></div>
|
||||||
|
<p class="stage-desc">ESO, AWS Load Balancer Controller, and metrics-server finish the job.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- (b) inputs + (c) command -->
|
||||||
|
<div class="subgrid">
|
||||||
|
<div class="well">
|
||||||
|
<p class="well-hd">1 · Inputs on disk</p>
|
||||||
|
<div class="filetree">planpalto-helm/
|
||||||
|
├─ Chart.yaml
|
||||||
|
├─ values.yaml <span class="an">apps: map (5 entries) + deps + global</span>
|
||||||
|
├─ templates/
|
||||||
|
│ ├─ namespace.yaml
|
||||||
|
│ ├─ apps.yaml <span class="acc">{{ range .Values.apps }}</span>
|
||||||
|
│ ├─ deps.yaml <span class="an">redis, nats</span>
|
||||||
|
│ ├─ external-secrets.yaml
|
||||||
|
│ ├─ ingress.yaml
|
||||||
|
│ └─ seed-job.yaml
|
||||||
|
└─ _helpers.tpl <span class="an">planpal.namespace / .storeName / .image</span>
|
||||||
|
+ override: <span class="acc">-f envs/a.yaml</span></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="cmd-wrap">
|
||||||
|
<p class="cmd-lead">2 · command ↓</p>
|
||||||
|
<div class="cmd-box"><span>helm upgrade --install </span><span class="acc">planpal</span><span> ./planpalto-helm -f </span><span class="acc">envs/a.yaml</span></div>
|
||||||
|
<p class="cmd-note">upgrade --install = create if absent, else new revision.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- (d) render well -->
|
||||||
|
<div class="well" style="margin-top:16px">
|
||||||
|
<p class="well-hd">3 · Render — Go template engine (client-side, in Helm)</p>
|
||||||
|
<div class="render3">
|
||||||
|
<div class="rcol" data-cross="merge">
|
||||||
|
<p class="rhd">Merged values</p>
|
||||||
|
<pre>values.yaml
|
||||||
|
+ <span class="acc">-f envs/a.yaml</span> (override)
|
||||||
|
+ <span class="acc">.Release.Name planpal</span>
|
||||||
|
+ <span class="acc">.Release.Namespace</span></pre>
|
||||||
|
</div>
|
||||||
|
<div class="rcol" data-cross="walk templates/">
|
||||||
|
<p class="rhd">Go template engine</p>
|
||||||
|
<ul class="mech">
|
||||||
|
<li><span class="tok">{{ .Values.x }}</span> <span class="gl">substitute</span></li>
|
||||||
|
<li><span class="tok">{{ if }} / {{ with }}</span> <span class="gl">conditional / scope</span></li>
|
||||||
|
<li><span class="tok">{{ range }}</span> <span class="gl">loop over apps map</span></li>
|
||||||
|
<li><span class="tok">{{ include "planpal.namespace" . }}</span> <span class="gl">named template from _helpers.tpl</span></li>
|
||||||
|
<li><span class="tok">| default | toJson | toString</span> <span class="gl">pipes</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="rcol">
|
||||||
|
<p class="rhd">Multi-doc YAML</p>
|
||||||
|
<pre>---
|
||||||
|
kind: Namespace
|
||||||
|
...
|
||||||
|
---
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: <span class="acc">planpal-backend</span>
|
||||||
|
...
|
||||||
|
--- <span class="dim">(× many docs)</span></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- (g) reconcile + (h) add-ons -->
|
||||||
|
<div class="twin">
|
||||||
|
<div class="well">
|
||||||
|
<p class="well-hd">5 · Reconcile</p>
|
||||||
|
<div class="chain">
|
||||||
|
<span class="bx">Deployment</span><span class="ar">→</span>
|
||||||
|
<span class="bx">ReplicaSet</span><span class="ar">→</span>
|
||||||
|
<span class="bx">Pod</span><span class="ar">→</span>
|
||||||
|
<span class="bx">scheduler</span><span class="ar">→</span>
|
||||||
|
<span class="bx">EKS node</span>
|
||||||
|
</div>
|
||||||
|
<p class="cmd-note">Controllers reconcile the declared state. HPA scales replicas via metrics-server.</p>
|
||||||
|
</div>
|
||||||
|
<div class="well">
|
||||||
|
<p class="well-hd">6 · Cluster add-ons</p>
|
||||||
|
<div class="addon-row"><span class="obj">ExternalSecret</span><span class="ar">▶ ESO</span><span class="gl">reads AWS Secrets Manager → real k8s Secret</span></div>
|
||||||
|
<div class="addon-row"><span class="obj">Ingress</span><span class="ar">▶ ALB ctrl</span><span class="gl">provisions an ALB</span></div>
|
||||||
|
<div class="addon-row"><span class="obj">HPA</span><span class="ar">▶ metrics-server</span><span class="gl">feeds CPU/mem → scale</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="sep">
|
||||||
|
|
||||||
|
<!-- (e) BOM table -->
|
||||||
|
<p class="secthead">Template → object map<span class="st-sub">bill of materials · rendered from the chart, exact counts</span></p>
|
||||||
|
<div class="bom-wrap">
|
||||||
|
<table class="bom">
|
||||||
|
<thead><tr><th style="width:32%">Source file</th><th>Rendered objects</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>templates/namespace.yaml</td><td>1 Namespace · 1 ServiceAccount <span class="dim">planpal-backend</span></td></tr>
|
||||||
|
<tr><td>templates/apps.yaml</td><td>5 Deployment · 2 Service · 5 HPA · 2 PDB <span class="note">← one {{ range }} loop, see DETAIL A</span></td></tr>
|
||||||
|
<tr><td>templates/deps.yaml</td><td>2 Deployment <span class="dim">redis, nats</span> · 2 Service <span class="note-dim">(these 2 also count toward the 7 Deployment total)</span></td></tr>
|
||||||
|
<tr><td>templates/external-secrets.yaml</td><td>1 ClusterSecretStore · 4 ExternalSecret</td></tr>
|
||||||
|
<tr><td>templates/ingress.yaml</td><td>1 Ingress <span class="dim">(ALB)</span></td></tr>
|
||||||
|
<tr><td>templates/seed-job.yaml</td><td>1 Job <span class="dim">seed-admin (runs once)</span></td></tr>
|
||||||
|
<tr class="sum"><td>Σ total</td><td>7 Deployment · 4 Service · 5 HPA · 2 PDB · 1 Ingress · 1 Job · 1 Namespace · 1 ServiceAccount · 1 ClusterSecretStore · 4 ExternalSecret</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="sep">
|
||||||
|
|
||||||
|
<!-- lower band: DETAIL A | DETAIL B -->
|
||||||
|
<div class="lower">
|
||||||
|
|
||||||
|
<!-- (f) DETAIL A -->
|
||||||
|
<div class="detail">
|
||||||
|
<span class="detail-tab">Detail A</span>
|
||||||
|
<p class="detail-cap">One <code>{{ range .Values.apps }}</code> over a 5-entry map builds 5 Deployments. Conditionals add the extras: a Service <code>if service:</code>, an HPA <code>if hpa:</code>, a PDB <code>if pdb:</code>.</p>
|
||||||
|
<div class="fan">
|
||||||
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<marker id="ah" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
|
||||||
|
<path d="M0,0 L6,3 L0,6 Z" fill="var(--accent)"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<!-- fan: single origin at bracket right-mid (x~40.5), 5 lines to lane mids -->
|
||||||
|
<line class="draw" style="--len:60" x1="40.5" y1="50" x2="52" y2="10" stroke="var(--accent)" stroke-width="0.5" marker-end="url(#ah)"/>
|
||||||
|
<line class="draw" style="--len:55" x1="40.5" y1="50" x2="52" y2="30" stroke="var(--accent)" stroke-width="0.5" marker-end="url(#ah)"/>
|
||||||
|
<line class="draw" style="--len:50" x1="40.5" y1="50" x2="52" y2="50" stroke="var(--accent)" stroke-width="0.5" marker-end="url(#ah)"/>
|
||||||
|
<line class="draw" style="--len:55" x1="40.5" y1="50" x2="52" y2="70" stroke="var(--accent)" stroke-width="0.5" marker-end="url(#ah)"/>
|
||||||
|
<line class="draw" style="--len:60" x1="40.5" y1="50" x2="52" y2="90" stroke="var(--accent)" stroke-width="0.5" marker-end="url(#ah)"/>
|
||||||
|
</svg>
|
||||||
|
<div class="fan-grid">
|
||||||
|
<!-- Zone L: apps map -->
|
||||||
|
<div class="lanes">
|
||||||
|
<div class="srcbox"><span class="nm">backend</span><span class="tags"><span class="tag">service:</span><span class="tag">hpa:</span><span class="tag">pdb:</span></span></div>
|
||||||
|
<div class="srcbox"><span class="nm">frontend</span><span class="tags"><span class="tag">service:</span><span class="tag">hpa:</span><span class="tag">pdb:</span></span></div>
|
||||||
|
<div class="srcbox"><span class="nm">schedule-worker</span><span class="tags"><span class="tag">hpa:</span></span></div>
|
||||||
|
<div class="srcbox"><span class="nm">notification-worker</span><span class="tags"><span class="tag">hpa:</span></span></div>
|
||||||
|
<div class="srcbox"><span class="nm">ai-worker</span><span class="tags"><span class="tag">hpa:</span></span></div>
|
||||||
|
</div>
|
||||||
|
<!-- Zone C: range bracket -->
|
||||||
|
<div class="bracket-col">
|
||||||
|
<span class="bracket-lbl">{{ range .Values.apps }} ×5</span>
|
||||||
|
</div>
|
||||||
|
<!-- Zone R: emitted objects -->
|
||||||
|
<div class="outlanes">
|
||||||
|
<div class="oline"><span class="objbox dep">Deployment</span><span class="gate">if service:</span><span class="objbox">Service</span><span class="gate">if hpa:</span><span class="objbox">HPA</span><span class="gate">if pdb:</span><span class="objbox">PDB</span></div>
|
||||||
|
<div class="oline"><span class="objbox dep">Deployment</span><span class="gate">if service:</span><span class="objbox">Service</span><span class="gate">if hpa:</span><span class="objbox">HPA</span><span class="gate">if pdb:</span><span class="objbox">PDB</span></div>
|
||||||
|
<div class="oline"><span class="objbox dep">Deployment</span><span class="gate">if hpa:</span><span class="objbox">HPA</span></div>
|
||||||
|
<div class="oline"><span class="objbox dep">Deployment</span><span class="gate">if hpa:</span><span class="objbox">HPA</span></div>
|
||||||
|
<div class="oline"><span class="objbox dep">Deployment</span><span class="gate">if hpa:</span><span class="objbox">HPA</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="fan-recon"><code>apps.yaml → 5 Deployment · 2 Service · 5 HPA · 2 PDB.</code> The redis + nats Deployments come from <code>deps.yaml</code>, not this loop — they are the other 2 in the total of 7.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- (i) DETAIL B -->
|
||||||
|
<div class="detail">
|
||||||
|
<span class="detail-tab">Detail B</span>
|
||||||
|
<p class="detail-cap">Each <code>helm install</code> lands in its own namespace + ALB + ClusterSecretStore, all defaulting to <code>.Release.Name</code> via <code>_helpers.tpl</code>.</p>
|
||||||
|
<div class="rel-cols">
|
||||||
|
<div class="rel-col">
|
||||||
|
<p class="rel-title">planpal-a</p>
|
||||||
|
<div class="rel-stack">
|
||||||
|
<div class="rel-cell">Namespace<br><span class="v">planpal-a</span></div>
|
||||||
|
<div class="rel-cell">Ingress → ALB<br><span class="dim">(own LB)</span></div>
|
||||||
|
<div class="rel-cell">ClusterSecretStore<br><span class="v">planpal-a-aws-planpal</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rel-col">
|
||||||
|
<p class="rel-title">planpal-b</p>
|
||||||
|
<div class="rel-stack">
|
||||||
|
<div class="rel-cell">Namespace<br><span class="v">planpal-b</span></div>
|
||||||
|
<div class="rel-cell">Ingress → ALB<br><span class="dim">(own LB)</span></div>
|
||||||
|
<div class="rel-cell">ClusterSecretStore<br><span class="v">planpal-b-aws-planpal</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="rel-note">planpal.namespace = default .Release.Name → <span class="v">planpal-a</span> / <span class="v">planpal-b</span>
|
||||||
|
planpal.storeName = per-release CSS
|
||||||
|
|
||||||
|
The empty gutter is the isolation: no
|
||||||
|
connector crosses it. Two releases share
|
||||||
|
nothing on the cluster.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- title block -->
|
||||||
|
<div class="tblock-wrap">
|
||||||
|
<div class="tblock">
|
||||||
|
<div class="tb-row"><div class="tb-k">Chart</div><div class="tb-v">planpalto-helm</div></div>
|
||||||
|
<div class="tb-row"><div class="tb-k">Release</div><div class="tb-v v-acc">planpal</div></div>
|
||||||
|
<div class="tb-row"><div class="tb-k">Cluster</div><div class="tb-v">fakhri-k8 (EKS)</div></div>
|
||||||
|
<div class="tb-row"><div class="tb-k">Release state</div><div class="tb-v">Secret sh.helm.release.v1</div></div>
|
||||||
|
<div class="tb-row"><div class="tb-k">Scale</div><div class="tb-v">NTS</div></div>
|
||||||
|
<div class="tb-row"><div class="tb-k">Sheet</div><div class="tb-v">1 of 1</div></div>
|
||||||
|
<div class="tb-row"><div class="tb-k">Date</div><div class="tb-v">2026-09-01</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div></div>
|
||||||
30
planpalto-helm/gitea/gitea-ingress.yaml
Normal file
30
planpalto-helm/gitea/gitea-ingress.yaml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Gitea UI ingress. Reuses the planpal-helm ALB group, so no new load balancer.
|
||||||
|
# Own ACM cert (single-name, like the argocd and app certs).
|
||||||
|
# kubectl apply -f planpalto-helm/gitea/gitea-ingress.yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: gitea
|
||||||
|
namespace: gitea
|
||||||
|
annotations:
|
||||||
|
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||||
|
alb.ingress.kubernetes.io/target-type: ip
|
||||||
|
alb.ingress.kubernetes.io/backend-protocol: HTTP
|
||||||
|
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80},{"HTTPS":443}]'
|
||||||
|
alb.ingress.kubernetes.io/ssl-redirect: '443'
|
||||||
|
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-southeast-3:134604498185:certificate/098ca0ec-65c8-44bb-952a-2cb86b6307d8
|
||||||
|
alb.ingress.kubernetes.io/healthcheck-path: /api/healthz
|
||||||
|
alb.ingress.kubernetes.io/group.name: planpal-helm
|
||||||
|
spec:
|
||||||
|
ingressClassName: alb
|
||||||
|
rules:
|
||||||
|
- host: fakhri-gitea.sandbox.devopsinstitute.id
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: gitea-http
|
||||||
|
port:
|
||||||
|
number: 3000
|
||||||
75
planpalto-helm/gitea/gitea.yaml
Normal file
75
planpalto-helm/gitea/gitea.yaml
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Gitea git host for Argo CD. SQLite, single pod, one PVC. Installed OUTSIDE
|
||||||
|
# Argo (plain kubectl apply) because Argo pulls its manifests FROM Gitea, so
|
||||||
|
# Gitea cannot be an Argo-managed app in that same repo.
|
||||||
|
#
|
||||||
|
# kubectl apply -f planpalto-helm/gitea/gitea.yaml
|
||||||
|
#
|
||||||
|
# Sizing: ~128Mi idle, fits a t3.medium next to PlanPal + Argo.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: gitea
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: gitea-data
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
accessModes: [ReadWriteOnce]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
# storageClassName omitted -> cluster default (gp2/gp3 on EKS).
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: gitea
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate # single RWO volume, no rolling two-pod overlap
|
||||||
|
selector:
|
||||||
|
matchLabels: { app: gitea }
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels: { app: gitea }
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: gitea
|
||||||
|
image: gitea/gitea:1.22
|
||||||
|
env:
|
||||||
|
- { name: GITEA__database__DB_TYPE, value: sqlite3 }
|
||||||
|
- { name: GITEA__server__ROOT_URL, value: "http://gitea-http.gitea.svc.cluster.local:3000/" }
|
||||||
|
- { name: GITEA__server__DISABLE_SSH, value: "true" } # HTTP-only; Argo uses HTTP
|
||||||
|
- { name: GITEA__service__DISABLE_REGISTRATION, value: "true" }
|
||||||
|
ports:
|
||||||
|
- { name: http, containerPort: 3000 }
|
||||||
|
volumeMounts:
|
||||||
|
- { name: data, mountPath: /data }
|
||||||
|
resources:
|
||||||
|
requests: { cpu: 100m, memory: 128Mi }
|
||||||
|
limits: { cpu: 500m, memory: 512Mi }
|
||||||
|
readinessProbe:
|
||||||
|
httpGet: { path: /api/healthz, port: 3000 }
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet: { path: /api/healthz, port: 3000 }
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 15
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim: { claimName: gitea-data }
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: gitea-http
|
||||||
|
namespace: gitea
|
||||||
|
spec:
|
||||||
|
selector: { app: gitea }
|
||||||
|
ports:
|
||||||
|
- { name: http, port: 3000, targetPort: 3000 }
|
||||||
17
planpalto-helm/templates/_helpers.tpl
Normal file
17
planpalto-helm/templates/_helpers.tpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{{/* Full image ref for an app entry: picks backend or frontend repo+tag. */}}
|
||||||
|
{{- define "planpal.image" -}}
|
||||||
|
{{- $img := index $.Values.image .which -}}
|
||||||
|
{{- printf "%s/%s:%s" $.Values.image.registry $img.repository (toString $img.tag) -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/* Namespace: explicit value, else the release name. Lets each release be
|
||||||
|
self-contained so multiple installs don't collide. */}}
|
||||||
|
{{- define "planpal.namespace" -}}
|
||||||
|
{{- .Values.namespace | default .Release.Name -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/* ClusterSecretStore name: cluster-scoped, so it must be unique per release.
|
||||||
|
Explicit value, else "<release>-aws-planpal". */}}
|
||||||
|
{{- define "planpal.storeName" -}}
|
||||||
|
{{- .Values.externalSecrets.storeName | default (printf "%s-aws-planpal" .Release.Name) -}}
|
||||||
|
{{- end -}}
|
||||||
90
planpalto-helm/templates/apps.yaml
Normal file
90
planpalto-helm/templates/apps.yaml
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{{- range $name, $app := .Values.apps }}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
namespace: {{ include "planpal.namespace" $ }}
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "1"
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels: { app: {{ $name }} }
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels: { app: {{ $name }} }
|
||||||
|
spec:
|
||||||
|
{{- if eq $app.image "backend" }}
|
||||||
|
serviceAccountName: {{ $.Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: {{ $name }}
|
||||||
|
image: {{ include "planpal.image" (dict "Values" $.Values "which" $app.image) }}
|
||||||
|
imagePullPolicy: {{ $.Values.image.pullPolicy }}
|
||||||
|
{{- with $app.command }}
|
||||||
|
command: {{ toJson . }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
{{- range $app.ports }}
|
||||||
|
- {{ toJson . }}
|
||||||
|
{{- end }}
|
||||||
|
envFrom:
|
||||||
|
{{- range $app.envFrom }}
|
||||||
|
- secretRef: { name: {{ . }} }
|
||||||
|
{{- end }}
|
||||||
|
{{- if $app.probePath }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet: { path: {{ $app.probePath }}, port: {{ $app.probePort }} }
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet: { path: {{ $app.probePath }}, port: {{ $app.probePort }} }
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 10
|
||||||
|
{{- end }}
|
||||||
|
resources: {{ toJson $app.resources }}
|
||||||
|
---
|
||||||
|
{{- if $app.service }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
namespace: {{ include "planpal.namespace" $ }}
|
||||||
|
{{- with $app.service.annotations }}
|
||||||
|
annotations: {{ toJson . }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
selector: { app: {{ $name }} }
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ $app.service.port }}
|
||||||
|
targetPort: {{ $app.service.targetPort }}
|
||||||
|
---
|
||||||
|
{{- end }}
|
||||||
|
{{- with $app.hpa }}
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
namespace: {{ include "planpal.namespace" $ }}
|
||||||
|
spec:
|
||||||
|
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: {{ $name }} }
|
||||||
|
minReplicas: {{ .min }}
|
||||||
|
maxReplicas: {{ .max }}
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource: { name: cpu, target: { type: Utilization, averageUtilization: {{ .cpu }} } }
|
||||||
|
---
|
||||||
|
{{- end }}
|
||||||
|
{{- with $app.pdb }}
|
||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
namespace: {{ include "planpal.namespace" $ }}
|
||||||
|
spec:
|
||||||
|
maxUnavailable: {{ .maxUnavailable }}
|
||||||
|
selector:
|
||||||
|
matchLabels: { app: {{ $name }} }
|
||||||
|
---
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
75
planpalto-helm/templates/deps.yaml
Normal file
75
planpalto-helm/templates/deps.yaml
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: {{ include "planpal.namespace" . }}
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels: { app: redis }
|
||||||
|
template:
|
||||||
|
metadata: { labels: { app: redis } }
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: {{ .Values.redis.image }}
|
||||||
|
ports: [{ containerPort: 6379 }]
|
||||||
|
resources: {{ toJson .Values.redis.resources }}
|
||||||
|
readinessProbe:
|
||||||
|
exec: { command: ["redis-cli", "ping"] }
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
exec: { command: ["redis-cli", "ping"] }
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 10
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: {{ include "planpal.namespace" . }}
|
||||||
|
spec:
|
||||||
|
selector: { app: redis }
|
||||||
|
ports: [{ port: 6379 }]
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nats
|
||||||
|
namespace: {{ include "planpal.namespace" . }}
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels: { app: nats }
|
||||||
|
template:
|
||||||
|
metadata: { labels: { app: nats } }
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nats
|
||||||
|
image: {{ .Values.nats.image }}
|
||||||
|
args: ["-m", "8222"]
|
||||||
|
ports:
|
||||||
|
- containerPort: 4222
|
||||||
|
- containerPort: 8222
|
||||||
|
readinessProbe:
|
||||||
|
httpGet: { path: "/healthz", port: 8222 }
|
||||||
|
periodSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket: { port: 4222 }
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 10
|
||||||
|
resources: {{ toJson .Values.nats.resources }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nats
|
||||||
|
namespace: {{ include "planpal.namespace" . }}
|
||||||
|
spec:
|
||||||
|
selector: { app: nats }
|
||||||
|
ports:
|
||||||
|
- port: 4222
|
||||||
|
name: client
|
||||||
32
planpalto-helm/templates/external-secrets.yaml
Normal file
32
planpalto-helm/templates/external-secrets.yaml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{{- if .Values.externalSecrets.enabled }}
|
||||||
|
apiVersion: external-secrets.io/v1
|
||||||
|
kind: ClusterSecretStore
|
||||||
|
metadata:
|
||||||
|
name: {{ include "planpal.storeName" . }}
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
provider:
|
||||||
|
aws:
|
||||||
|
service: SecretsManager
|
||||||
|
region: {{ .Values.externalSecrets.region }}
|
||||||
|
# credentials come from the ESO controller's Pod Identity role; no keys here.
|
||||||
|
{{- range $target, $key := .Values.externalSecrets.secrets }}
|
||||||
|
---
|
||||||
|
apiVersion: external-secrets.io/v1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: {{ $target }}
|
||||||
|
namespace: {{ include "planpal.namespace" $ }}
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
refreshInterval: {{ $.Values.externalSecrets.refreshInterval }}
|
||||||
|
secretStoreRef: { name: {{ include "planpal.storeName" $ }}, kind: ClusterSecretStore }
|
||||||
|
target:
|
||||||
|
name: {{ $target }}
|
||||||
|
creationPolicy: Owner
|
||||||
|
dataFrom:
|
||||||
|
- extract: { key: {{ $key }} }
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
37
planpalto-helm/templates/ingress.yaml
Normal file
37
planpalto-helm/templates/ingress.yaml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: planpal
|
||||||
|
namespace: {{ include "planpal.namespace" . }}
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "2"
|
||||||
|
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||||
|
alb.ingress.kubernetes.io/target-type: ip
|
||||||
|
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80},{"HTTPS":443}]'
|
||||||
|
alb.ingress.kubernetes.io/certificate-arn: {{ .Values.ingress.certArn }}
|
||||||
|
alb.ingress.kubernetes.io/healthcheck-path: {{ .Values.ingress.healthcheckPath }}
|
||||||
|
# ALB per release: group.name defaults to the release name, so each install
|
||||||
|
# gets its own load balancer. Set the same ingress.groupName on two releases
|
||||||
|
# to make them share one ALB instead.
|
||||||
|
alb.ingress.kubernetes.io/group.name: {{ .Values.ingress.groupName | default .Release.Name }}
|
||||||
|
{{- if .Values.ingress.externalDNS }}
|
||||||
|
# external-dns creates the Route53 record for this host from the rule below.
|
||||||
|
# This annotation is only needed when the record name differs from the host.
|
||||||
|
external-dns.alb.ingress.kubernetes.io/hostname: {{ .Values.ingress.host }}
|
||||||
|
{{- end }}
|
||||||
|
# ponytail: HTTP:80 and HTTPS:443 both open, no forced redirect. Add the
|
||||||
|
# ssl-redirect annotation later to push 80 -> 443.
|
||||||
|
spec:
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /api/v1
|
||||||
|
pathType: Prefix
|
||||||
|
backend: { service: { name: backend, port: { number: {{ .Values.apps.backend.service.port }} } } }
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend: { service: { name: frontend, port: { number: {{ .Values.apps.frontend.service.port }} } } }
|
||||||
|
{{- end }}
|
||||||
14
planpalto-helm/templates/namespace.yaml
Normal file
14
planpalto-helm/templates/namespace.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{{- if .Values.createNamespace }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: {{ include "planpal.namespace" . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.serviceAccount.name }}
|
||||||
|
namespace: {{ include "planpal.namespace" . }}
|
||||||
|
{{- end }}
|
||||||
28
planpalto-helm/templates/seed-job.yaml
Normal file
28
planpalto-helm/templates/seed-job.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{{- if .Values.seedJob.enabled }}
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: seed-admin
|
||||||
|
namespace: {{ include "planpal.namespace" . }}
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "3"
|
||||||
|
# Argo re-runs a Job by hash; a spec change makes a new Job. Delete the old
|
||||||
|
# one before sync if it lingers, or set this to a Sync hook if you want it
|
||||||
|
# to run every sync. ponytail: kept as a plain waved object, not a hook.
|
||||||
|
spec:
|
||||||
|
backoffLimit: 10
|
||||||
|
ttlSecondsAfterFinished: 300
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: {{ .Values.serviceAccount.name }}
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
containers:
|
||||||
|
- name: seed-admin
|
||||||
|
image: {{ include "planpal.image" (dict "Values" .Values "which" "backend") }}
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
command: {{ toJson .Values.seedJob.command }}
|
||||||
|
envFrom:
|
||||||
|
{{- range .Values.seedJob.envFrom }}
|
||||||
|
- secretRef: { name: {{ . }} }
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
25
planpalto-helm/values-prod.yaml
Normal file
25
planpalto-helm/values-prod.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Prod overrides. Everything else inherits values.yaml.
|
||||||
|
# Keeps the existing live host and ALB group (planpal-helm) so this env
|
||||||
|
# adopts the running deployment instead of standing up a second one.
|
||||||
|
namespace: planpal-prod
|
||||||
|
|
||||||
|
image:
|
||||||
|
frontend:
|
||||||
|
tag: latest
|
||||||
|
backend:
|
||||||
|
tag: latest
|
||||||
|
|
||||||
|
externalSecrets:
|
||||||
|
storeName: prod-aws-planpal
|
||||||
|
secrets:
|
||||||
|
backend-env: planpal/backend-env
|
||||||
|
aws-env: planpal/aws-env
|
||||||
|
seed-env: planpal/seed-env
|
||||||
|
frontend-env: planpal/frontend
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
host: fakhri-k8.sandbox.devopsinstitute.id
|
||||||
|
groupName: planpal-helm # existing live ALB group
|
||||||
|
|
||||||
|
seedJob:
|
||||||
|
enabled: true
|
||||||
29
planpalto-helm/values-staging.yaml
Normal file
29
planpalto-helm/values-staging.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Staging overrides. Everything else inherits values.yaml.
|
||||||
|
# Pinned explicitly (not left to release-name defaults) so the two envs
|
||||||
|
# provably never share a namespace, ALB, or cluster-scoped store.
|
||||||
|
namespace: planpal-staging
|
||||||
|
|
||||||
|
image:
|
||||||
|
frontend:
|
||||||
|
tag: latest
|
||||||
|
backend:
|
||||||
|
tag: latest
|
||||||
|
|
||||||
|
externalSecrets:
|
||||||
|
storeName: staging-aws-planpal
|
||||||
|
# ponytail: staging points at the SAME planpal/* SM keys as prod for now.
|
||||||
|
# Give staging its own DB/config by pointing these at planpal-staging/* keys
|
||||||
|
# and letting the ESO role read them.
|
||||||
|
secrets:
|
||||||
|
backend-env: planpal/backend-env
|
||||||
|
aws-env: planpal/aws-env
|
||||||
|
seed-env: planpal/seed-env
|
||||||
|
frontend-env: planpal/frontend
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
host: staging.fakhri-k8.sandbox.devopsinstitute.id
|
||||||
|
groupName: planpal-staging # own ALB, separate from prod
|
||||||
|
|
||||||
|
# Seed the admin on staging too. Flip to false once seeded.
|
||||||
|
seedJob:
|
||||||
|
enabled: true
|
||||||
140
planpalto-helm/values.yaml
Normal file
140
planpalto-helm/values.yaml
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# PlanPal Helm values. Mirrors planpalto-infrato/k8s + kustomization.yaml.
|
||||||
|
# namespace empty -> defaults to the release name, so multiple installs
|
||||||
|
# (helm install planpal-a / planpal-b) land in separate namespaces and don't
|
||||||
|
# collide. Set it to pin a fixed namespace.
|
||||||
|
namespace: ""
|
||||||
|
createNamespace: true
|
||||||
|
|
||||||
|
image:
|
||||||
|
registry: 134604498185.dkr.ecr.ap-southeast-3.amazonaws.com
|
||||||
|
backend:
|
||||||
|
repository: fakhri-planpal-backend
|
||||||
|
tag: latest
|
||||||
|
frontend:
|
||||||
|
repository: fakhri-planpal-frontend
|
||||||
|
tag: latest # ECR frontend repo only publishes latest (1.1 was a minikube-local tag)
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
name: planpal-backend # Pod Identity -> role fakhri-app-planpal
|
||||||
|
create: true
|
||||||
|
|
||||||
|
# App workloads. backend+workers share the backend image and mount backend-env+aws-env.
|
||||||
|
# frontend uses the frontend image and frontend-env.
|
||||||
|
apps:
|
||||||
|
backend:
|
||||||
|
command: [planpal-server]
|
||||||
|
image: backend
|
||||||
|
envFrom: [backend-env, aws-env]
|
||||||
|
ports:
|
||||||
|
- { name: http, containerPort: 8088 }
|
||||||
|
- { name: metrics, containerPort: 9090 }
|
||||||
|
service:
|
||||||
|
port: 8088
|
||||||
|
targetPort: 8088
|
||||||
|
probePath: /api/v1/health
|
||||||
|
probePort: 8088
|
||||||
|
resources:
|
||||||
|
requests: { cpu: 25m, memory: 64Mi }
|
||||||
|
limits: { cpu: 250m, memory: 256Mi }
|
||||||
|
hpa: { min: 2, max: 5, cpu: 50 }
|
||||||
|
pdb: { maxUnavailable: 1 }
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
command: []
|
||||||
|
image: frontend
|
||||||
|
envFrom: [frontend-env]
|
||||||
|
ports:
|
||||||
|
- { name: http, containerPort: 3000 }
|
||||||
|
service:
|
||||||
|
port: 3000
|
||||||
|
targetPort: 3000
|
||||||
|
annotations:
|
||||||
|
alb.ingress.kubernetes.io/healthcheck-path: "/"
|
||||||
|
alb.ingress.kubernetes.io/success-codes: "200"
|
||||||
|
probePath: /
|
||||||
|
probePort: 3000
|
||||||
|
resources:
|
||||||
|
requests: { cpu: 25m, memory: 96Mi }
|
||||||
|
limits: { cpu: 300m, memory: 384Mi }
|
||||||
|
hpa: { min: 2, max: 5, cpu: 50 }
|
||||||
|
pdb: { maxUnavailable: 1 }
|
||||||
|
|
||||||
|
schedule-worker:
|
||||||
|
command: [planpal-schedule-worker]
|
||||||
|
image: backend
|
||||||
|
envFrom: [backend-env, aws-env]
|
||||||
|
ports: [{ name: metrics, containerPort: 9091 }]
|
||||||
|
resources:
|
||||||
|
requests: { cpu: 25m, memory: 64Mi }
|
||||||
|
limits: { cpu: 200m, memory: 256Mi }
|
||||||
|
hpa: { min: 1, max: 3, cpu: 60 }
|
||||||
|
|
||||||
|
notification-worker:
|
||||||
|
command: [planpal-notification-worker]
|
||||||
|
image: backend
|
||||||
|
envFrom: [backend-env, aws-env]
|
||||||
|
ports: [{ name: metrics, containerPort: 9092 }]
|
||||||
|
resources:
|
||||||
|
requests: { cpu: 25m, memory: 64Mi }
|
||||||
|
limits: { cpu: 200m, memory: 256Mi }
|
||||||
|
hpa: { min: 1, max: 3, cpu: 60 }
|
||||||
|
|
||||||
|
ai-worker:
|
||||||
|
command: [planpal-ai-worker]
|
||||||
|
image: backend
|
||||||
|
envFrom: [backend-env, aws-env]
|
||||||
|
ports: [{ name: metrics, containerPort: 9093 }]
|
||||||
|
resources:
|
||||||
|
requests: { cpu: 25m, memory: 64Mi }
|
||||||
|
limits: { cpu: 200m, memory: 256Mi }
|
||||||
|
hpa: { min: 1, max: 3, cpu: 60 }
|
||||||
|
|
||||||
|
# Stateless in-cluster deps.
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
resources:
|
||||||
|
requests: { cpu: 250m, memory: 256Mi }
|
||||||
|
limits: { cpu: 500m, memory: 512Mi }
|
||||||
|
nats:
|
||||||
|
image: nats:2-alpine
|
||||||
|
resources:
|
||||||
|
requests: { cpu: 100m, memory: 128Mi }
|
||||||
|
limits: { cpu: 200m, memory: 256Mi }
|
||||||
|
|
||||||
|
# External Secrets Operator: pull each k8s Secret from AWS Secrets Manager.
|
||||||
|
externalSecrets:
|
||||||
|
enabled: true
|
||||||
|
region: ap-southeast-3
|
||||||
|
# storeName empty -> "<release>-aws-planpal". The ClusterSecretStore is
|
||||||
|
# cluster-scoped, so it must be unique per release.
|
||||||
|
storeName: ""
|
||||||
|
refreshInterval: 1h
|
||||||
|
# target k8s Secret name -> Secrets Manager key. Releases share these keys
|
||||||
|
# (same DB/seed/config is fine). Point a release at other keys to isolate it.
|
||||||
|
secrets:
|
||||||
|
backend-env: planpal/backend-env
|
||||||
|
aws-env: planpal/aws-env
|
||||||
|
seed-env: planpal/seed-env
|
||||||
|
frontend-env: planpal/frontend
|
||||||
|
|
||||||
|
# Admin seed one-shot Job.
|
||||||
|
seedJob:
|
||||||
|
enabled: true
|
||||||
|
command: [seed_admin]
|
||||||
|
envFrom: [backend-env, seed-env]
|
||||||
|
|
||||||
|
# ALB ingress. host is the external DNS name; certArn is the ACM cert.
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: alb
|
||||||
|
host: fakhri-k8.sandbox.devopsinstitute.id
|
||||||
|
certArn: arn:aws:acm:ap-southeast-3:134604498185:certificate/c9fcfcb9-9d16-4dbb-989f-9f77f6496621
|
||||||
|
# groupName empty -> release name, so each install gets its own ALB. Set the
|
||||||
|
# same value on two releases to make them share one ALB.
|
||||||
|
groupName: ""
|
||||||
|
healthcheckPath: /api/v1/health
|
||||||
|
# externalDNS true adds the external-dns hostname annotation. The external-dns
|
||||||
|
# controller then creates the Route53 record for host. Needs external-dns
|
||||||
|
# installed and permitted on the target hosted zone.
|
||||||
|
externalDNS: false
|
||||||
Loading…
Reference in New Issue
Block a user