Skip to content

Commit 64f43e2

Browse files
authored
Merge pull request #375 from OpenMS/claude/parallel-webapp-memory-optimization-RoNnJ
Refactor K8s deployment to use memory-tier components
2 parents 971cfdd + 43c300b commit 64f43e2

19 files changed

Lines changed: 199 additions & 41 deletions

.claude/skills/configure-k8s-deployment.md

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,9 @@ Run `configure-app-settings` first. This skill assumes `settings.json`, the Dock
2020

2121
Push your changes to `main` or create a tag. The workflow `.github/workflows/build-and-test.yml` builds both the full and lightweight (`-simple`) variants and publishes them to `ghcr.io/<your-org>/<your-repo>:<tag>-full` and `:<tag>-simple` respectively. The unsuffixed `latest` tag tracks the full variant on the `main` branch.
2222

23-
2. **Copy the template overlay.**
23+
2. **Edit `k8s/overlays/prod/kustomization.yaml` in place.**
2424

25-
```bash
26-
cp -r k8s/overlays/template-app k8s/overlays/<your-app-name>
27-
```
28-
29-
3. **Edit `k8s/overlays/<your-app-name>/kustomization.yaml`**:
25+
Each fork ships a single production overlay at `k8s/overlays/prod/`. Edit the existing file rather than copying — the repo itself identifies the app, so there is no per-app overlay subdirectory.
3026

3127
- Change `namePrefix` from `template-app-` to `<your-app-name>-`
3228
- Change `commonLabels.app` from `template-app` to `<your-app-name>`
@@ -38,18 +34,31 @@ Run `configure-app-settings` first. This skill assumes `settings.json`, the Dock
3834

3935
The overlay leaves the nginx `Ingress` unpatched because production deployments use Traefik. If you are deploying to an nginx-only cluster, add an overlay patch for both `rules[].host` entries in the base `Ingress` (same `.de` / `.org` pattern) instead of the IngressRoute.
4036

37+
3. **Select a memory tier.**
38+
39+
The overlay pulls in one of two Kustomize components under `components:`. The default `memory-tier-low` pins the app to the low-mem node (~64 GB) with modest resource requests/limits — correct for most template-based apps. Switch to `memory-tier-high` only if the workload genuinely needs the large-memory node (DIA spectral-library construction, OpenSwath peak picking, DIA-LFQ):
40+
41+
```yaml
42+
components:
43+
- ../../components/memory-tier-low # default: light app
44+
# OR
45+
- ../../components/memory-tier-high # memory-intensive app
46+
```
47+
48+
The cluster node labels `openms.de/memory-tier=low` and `openms.de/memory-tier=high` must already exist; the tier component adds the matching `nodeSelector`.
49+
4150
4. **Validate the overlay builds.**
4251

4352
```bash
44-
kubectl kustomize k8s/overlays/<your-app-name>/
53+
kubectl kustomize k8s/overlays/prod/
4554
```
4655

4756
Should print the rendered manifests with no errors.
4857

4958
5. **Deploy.**
5059

5160
```bash
52-
kubectl apply -k k8s/overlays/<your-app-name>/
61+
kubectl apply -k k8s/overlays/prod/
5362
```
5463

5564
6. **Verify.**
@@ -63,18 +72,19 @@ Run `configure-app-settings` first. This skill assumes `settings.json`, the Dock
6372

6473
## Reference Files
6574

66-
- Overlay template: `k8s/overlays/template-app/kustomization.yaml`
75+
- Overlay: `k8s/overlays/prod/kustomization.yaml`
76+
- Memory-tier components: `k8s/components/memory-tier-{low,high}/`
6777
- Base manifests: `k8s/base/*.yaml`
6878
- CI: `.github/workflows/build-and-test.yml` (unified build + lint + kind integration), `.github/workflows/ghcr-cleanup.yml` (scheduled tag retention)
6979
- Full reference: see the "Developers Guide: Kubernetes Deployment" Documentation page in the running Streamlit app.
7080

7181
## Checklist
7282

7383
- [ ] Image built and pushed to GHCR (via CI or manual push to `main`/tag)
74-
- [ ] Overlay copied to `k8s/overlays/<your-app-name>/`
75-
- [ ] `namePrefix`, `commonLabels.app`, `images[0].newName` updated
84+
- [ ] `namePrefix`, `commonLabels.app`, `images[0].newName` updated in `k8s/overlays/prod/kustomization.yaml`
7685
- [ ] IngressRoute patch updated (both hostnames + service reference)
7786
- [ ] Redis URL updated in both Deployment patches
87+
- [ ] Memory tier selected (`memory-tier-low` or `memory-tier-high`)
7888
- [ ] `kubectl kustomize` succeeds
7989
- [ ] `kubectl apply -k` succeeds
8090
- [ ] All pods Running, `rollout status` succeeds

.github/kind-config.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
kind: Cluster
2+
apiVersion: kind.x-k8s.io/v1alpha4
3+
# Two-node cluster that mirrors the production memory-tier topology, so
4+
# the Build-and-Test job passes regardless of which tier a fork selects
5+
# in its overlay (memory-tier-low or memory-tier-high). Without both
6+
# labels present, flipping the overlay would leave pods Pending on the
7+
# single kind node.
8+
nodes:
9+
- role: control-plane
10+
# Multi-node kind clusters taint the control-plane with
11+
# node-role.kubernetes.io/control-plane:NoSchedule. Clear it so
12+
# app pods with nodeSelector memory-tier=low can actually land
13+
# here (single-node kind had no such taint, hence the original
14+
# workflow passed without this patch).
15+
kubeadmConfigPatches:
16+
- |
17+
kind: InitConfiguration
18+
nodeRegistration:
19+
taints: []
20+
labels:
21+
openms.de/memory-tier: low
22+
# ingress-ready is required by the kind variant of the
23+
# ingress-nginx deploy manifest applied in CI.
24+
ingress-ready: "true"
25+
- role: worker
26+
labels:
27+
openms.de/memory-tier: high

.github/workflows/build-and-test.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
3636
- name: Validate kustomized overlay output
3737
run: |
38-
kubectl kustomize k8s/overlays/template-app/ | \
38+
kubectl kustomize k8s/overlays/prod/ | \
3939
kubeconform -summary -strict -kubernetes-version 1.28.0 -skip IngressRoute
4040
4141
build:
@@ -105,6 +105,7 @@ jobs:
105105
uses: helm/kind-action@v1
106106
with:
107107
cluster_name: test-cluster
108+
config: .github/kind-config.yaml
108109

109110
- name: Load image into kind cluster
110111
run: kind load docker-image openms-streamlit:test --name test-cluster
@@ -117,7 +118,7 @@ jobs:
117118
- name: Deploy with Kustomize
118119
run: |
119120
# Filter out Traefik IngressRoute (kind cluster uses nginx) and force imagePullPolicy=Never
120-
kubectl kustomize k8s/overlays/template-app/ | \
121+
kubectl kustomize k8s/overlays/prod/ | \
121122
yq 'select(.kind != "IngressRoute")' | \
122123
sed 's|imagePullPolicy: IfNotPresent|imagePullPolicy: Never|g' | \
123124
sed 's|storageClassName: cinder-csi|storageClassName: standard|g' > /tmp/manifests.yaml
@@ -176,6 +177,7 @@ jobs:
176177
uses: helm/kind-action@v1
177178
with:
178179
cluster_name: traefik-test
180+
config: .github/kind-config.yaml
179181

180182
- name: Load image into kind cluster
181183
run: kind load docker-image openms-streamlit:test --name traefik-test
@@ -194,7 +196,7 @@ jobs:
194196
195197
- name: Deploy with Kustomize (full manifests, no filter)
196198
run: |
197-
kubectl kustomize k8s/overlays/template-app/ | \
199+
kubectl kustomize k8s/overlays/prod/ | \
198200
sed 's|imagePullPolicy: IfNotPresent|imagePullPolicy: Never|g' | \
199201
sed 's|storageClassName: cinder-csi|storageClassName: standard|g' > /tmp/manifests.yaml
200202
for i in 1 2 3 4 5; do

docs/kubernetes-deployment.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,15 +162,13 @@ Update `settings.json`, choose a Dockerfile, and update `README.md`. If you are
162162

163163
Push your changes to `main` or create a tag. The workflow `.github/workflows/build-and-test.yml` builds both the full (`Dockerfile`) and lightweight (`Dockerfile_simple`) variants and pushes each to `ghcr.io/<your-org>/<your-repo>` with variant-suffixed tags: `<branch>-full` / `<branch>-simple`, `v<version>-full` / `v<version>-simple`, and `<sha>-full` / `<sha>-simple`. The unsuffixed `latest` tag tracks the full variant on `main`.
164164

165-
### Step 3 — Create your overlay
165+
### Step 3 — Edit the production overlay
166166

167-
```bash
168-
cp -r k8s/overlays/template-app k8s/overlays/<your-app-name>
169-
```
167+
Each fork ships a single production overlay at `k8s/overlays/prod/`. Edit this file in place — the forked repository itself identifies the app, so no per-app overlay subdirectory is created.
170168

171169
### Step 4 — Edit `kustomization.yaml`
172170

173-
Open `k8s/overlays/<your-app-name>/kustomization.yaml` and change the following fields:
171+
Open `k8s/overlays/prod/kustomization.yaml` and change the following fields:
174172

175173
| Field | Set to |
176174
|-------|--------|
@@ -184,6 +182,19 @@ Open `k8s/overlays/<your-app-name>/kustomization.yaml` and change the following
184182

185183
The overlay leaves the nginx `Ingress` unpatched because Traefik is the production ingress. If you are deploying to an nginx-only cluster, add an overlay patch for both `rules[].host` entries in the base `Ingress` (same `.de` / `.org` pattern) instead of the IngressRoute patch.
186184

185+
### Step 4b — Select a memory tier
186+
187+
The overlay pulls in one of two Kustomize components under `components:`:
188+
189+
```yaml
190+
components:
191+
- ../../components/memory-tier-low # default: light app on low-mem node
192+
# OR
193+
- ../../components/memory-tier-high # memory-intensive app on high-mem node
194+
```
195+
196+
`memory-tier-low` is the right choice for most apps. Switch to `memory-tier-high` only if the workload genuinely needs tens of GB of RAM (DIA spectral-library + OpenSwath peak picking, DIA-LFQ). The tier component adds the matching `nodeSelector: openms.de/memory-tier=<tier>` plus `requests`/`limits` sized for that node, so cluster nodes must already be labelled `openms.de/memory-tier=low` / `...=high`.
197+
187198
### Step 5 — Configure the admin password (optional)
188199

189200
Skip this step if you don't need the "Save as Demo" feature. The Streamlit Deployment mounts the `streamlit-secrets` Secret with `optional: true`, so the pod starts either way — the admin UI simply reports "Admin not configured" when the Secret is absent.
@@ -213,18 +224,18 @@ Because the Secret is created outside Kustomize, the overlay's `namePrefix` does
213224
Copy the template and fill it in:
214225

215226
```bash
216-
cp k8s/base/streamlit-secrets.yaml.example k8s/overlays/<your-app-name>/streamlit-secrets.yaml
227+
cp k8s/base/streamlit-secrets.yaml.example k8s/overlays/prod/streamlit-secrets.yaml
217228
# edit the password
218229
```
219230

220-
Add `- streamlit-secrets.yaml` to the `resources:` list in `k8s/overlays/<your-app-name>/kustomization.yaml`. The filename `streamlit-secrets.yaml` is gitignored (`.gitignore`: `k8s/**/streamlit-secrets.yaml`); always confirm with `git status` before committing — never commit a filled-in copy.
231+
Add `- streamlit-secrets.yaml` to the `resources:` list in `k8s/overlays/prod/kustomization.yaml`. The filename `streamlit-secrets.yaml` is gitignored (`.gitignore`: `k8s/**/streamlit-secrets.yaml`); always confirm with `git status` before committing — never commit a filled-in copy.
221232

222233
When the Secret is managed through Kustomize, the overlay's `namePrefix` rewrites both the Secret name and the Deployment reference, so no manual renaming is needed.
223234

224235
### Step 6 — Deploy
225236

226237
```bash
227-
kubectl apply -k k8s/overlays/<your-app-name>/
238+
kubectl apply -k k8s/overlays/prod/
228239
```
229240

230241
### Step 7 — Verify
@@ -252,13 +263,13 @@ One unified workflow owns manifest lint, Docker build, push, and kind integratio
252263
- **Trigger:** pull request to `main`, push to `main`, push of a `v*` tag, or manual workflow dispatch.
253264
- **Job 1 — `lint-manifests`:**
254265
- `kubeconform` runs against `k8s/base/*.yaml` with strict mode and Kubernetes 1.28 schemas (excluding `kustomization.yaml` and the Traefik CRD `traefik-ingressroute.yaml`).
255-
- `kubectl kustomize k8s/overlays/template-app/` must succeed; the kustomized output is re-validated through `kubeconform` (with `IngressRoute` skipped).
266+
- `kubectl kustomize k8s/overlays/prod/` must succeed; the kustomized output is re-validated through `kubeconform` (with `IngressRoute` skipped).
256267
- Takes ~30s. Fails fast so manifest typos never trigger the hours-long full Docker build.
257268
- **Job 2 — `build`** (`needs: lint-manifests`, matrix over `[full, simple]`):
258269
- Builds `Dockerfile` (full, includes TOPP tools) or `Dockerfile_simple` (pyOpenMS only) depending on the matrix leg.
259270
- **Buildx registry cache** (`type=registry,…,mode=max`) stored at `ghcr.io/<repo>/cache:full` and `:simple`. A `cache-from` read is attempted on every event; `cache-to` write only on push/tag/workflow_dispatch (fork PRs can't write). Repeat builds with an unchanged Dockerfile finish in minutes.
260271
- **Push** on push/tag/workflow_dispatch events (not on PRs). Tags: `<branch>-full` / `<branch>-simple`, `v<version>-full` / `v<version>-simple`, `<sha>-full` / `<sha>-simple`. `latest` is emitted only for the full variant on push to `main`.
261-
- **Kind integration** runs per variant: creates a kind cluster, loads the just-built image, installs the nginx ingress controller, applies the kustomized `template-app` overlay (filtering Traefik `IngressRoute`, forcing `imagePullPolicy: Never` and `storageClassName: standard`), asserts Redis + deployments become ready, and curls both `.de` and `.org` hostnames through the nginx ingress to verify dual-host routing.
272+
- **Kind integration** runs per variant: creates a kind cluster, loads the just-built image, installs the nginx ingress controller, applies the kustomized `prod` overlay (filtering Traefik `IngressRoute`, forcing `imagePullPolicy: Never` and `storageClassName: standard`), asserts Redis + deployments become ready, and curls both `.de` and `.org` hostnames through the nginx ingress to verify dual-host routing.
262273
- **Job 3 — `traefik-integration`** (`needs: lint-manifests`, runs once on `Dockerfile_simple`): builds the simple image, brings up a second kind cluster, installs Traefik via Helm (`service.type=ClusterIP`), applies the full kustomized overlay without filtering the `IngressRoute` (still patching `imagePullPolicy: Never` and `storageClassName: standard` for kind compatibility), and curls both hostnames through Traefik. Catches IngressRoute-syntax regressions that the nginx-side test cannot.
263274
- **Auth:** uses the workflow's `GITHUB_TOKEN` for GHCR login and as a build argument for in-image private-resource access. Fork PRs skip login (their `GITHUB_TOKEN` is read-only) but can still read the public cache.
264275
- **PR behavior:** all three jobs run on pull requests. No tags are pushed and no cache is written. The kind integration still runs, exercising manifests end-to-end. If branch protection requires these checks, a failure blocks merge.

k8s/base/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ resources:
1414
- ingress.yaml
1515
- traefik-ingressroute.yaml
1616
- cleanup-cronjob.yaml
17+
- limitrange.yaml

k8s/base/limitrange.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: v1
2+
kind: LimitRange
3+
metadata:
4+
name: default-container-limits
5+
spec:
6+
limits:
7+
- type: Container
8+
defaultRequest:
9+
memory: "512Mi"
10+
cpu: "250m"
11+
default:
12+
memory: "2Gi"
13+
cpu: "2"
14+
max:
15+
memory: "200Gi"
16+
cpu: "20"

k8s/base/redis.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ spec:
2121
- containerPort: 6379
2222
resources:
2323
requests:
24-
memory: "64Mi"
25-
cpu: "50m"
24+
memory: "256Mi"
25+
cpu: "100m"
2626
limits:
2727
memory: "256Mi"
28-
cpu: "250m"
28+
cpu: "100m"
2929
readinessProbe:
3030
exec:
3131
command: ["redis-cli", "ping"]

k8s/base/rq-worker-deployment.yaml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,6 @@ spec:
4848
mountPath: /app/settings-overrides.json
4949
subPath: settings-overrides.json
5050
readOnly: true
51-
resources:
52-
requests:
53-
memory: "1Gi"
54-
cpu: "500m"
55-
limits:
56-
memory: "8Gi"
57-
cpu: "4"
5851
volumes:
5952
- name: workspaces
6053
persistentVolumeClaim:

k8s/base/streamlit-deployment.yaml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,6 @@ spec:
6565
port: 8501
6666
initialDelaySeconds: 30
6767
periodSeconds: 30
68-
resources:
69-
requests:
70-
memory: "1Gi"
71-
cpu: "500m"
72-
limits:
73-
memory: "8Gi"
74-
cpu: "4"
7568
volumes:
7669
- name: workspaces
7770
persistentVolumeClaim:
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: kustomize.config.k8s.io/v1alpha1
2+
kind: Component
3+
4+
patches:
5+
- path: nodeselector.yaml
6+
target:
7+
kind: Deployment
8+
- path: streamlit-resources.yaml
9+
target:
10+
kind: Deployment
11+
name: streamlit
12+
- path: worker-resources.yaml
13+
target:
14+
kind: Deployment
15+
name: rq-worker

0 commit comments

Comments
 (0)