Files
foxhunt/docs/plans/2026-03-04-minio-binary-distribution.md
jgrusewski cc209aec7e feat(infra): replace PVC binary distribution with MinIO S3 fetch
Eliminate foxhunt-binaries and training-binaries PVCs — services and
training jobs now fetch binaries from MinIO via rclone initContainers.
This removes L40S GPU autoscale-for-PVC-writes, removes RWO node
affinity constraints, and requires zero image rebuilds.

Changes:
- .gitlab-ci.yml: replace ~115 lines of binary-writer pod logic with
  aws s3 cp to MinIO (~25 lines)
- 8 service YAMLs + 2 GPU overlays: add rclone initContainer + emptyDir
- Training job template: MinIO rclone fetch replaces PVC copy
- Delete foxhunt-binaries-pvc.yaml and training-binaries-pvc.yaml
- Add s3.fxhnt.ai DNS record (Terraform) and nginx proxy block
- Replace pod-writer-deploy skill with MinIO-based deploy skill

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:46:57 +01:00

535 lines
17 KiB
Markdown

# MinIO Binary Distribution Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Replace PVC-based binary distribution with MinIO S3 fetch, eliminating the L40S autoscale-for-PVC-writes problem.
**Architecture:** Deploy job uploads compiled binaries to MinIO via `aws s3 cp`. Service pods and training jobs fetch their binary at startup via rclone initContainer into an emptyDir. Two binary PVCs are deleted.
**Tech Stack:** MinIO (in-cluster S3), rclone (in runtime images), aws-cli (in infra-runner)
---
### Task 1: Replace binary-writer pods with MinIO upload in deploy job
**Files:**
- Modify: `.gitlab-ci.yml:1564-1681` (deploy job binary section)
**Step 1: Delete PVC apply lines and entire binary-writer block**
In `.gitlab-ci.yml`, find and delete these two PVC apply lines (around line 1565-1566):
```yaml
- kubectl apply -f infra/k8s/storage/foxhunt-binaries-pvc.yaml
- kubectl apply -f infra/k8s/training/training-binaries-pvc.yaml
```
Then delete the entire block from `# ── Step 1: Copy binaries to PVCs` through `echo "=== Binary deployment complete ==="` (lines ~1567-1681). This removes all binary-writer/training-binary-writer pod logic.
**Step 2: Add MinIO upload block**
Insert in place of the deleted block (same indentation level as other `- |` script blocks in the deploy job):
```yaml
# ── Step 1: Upload binaries to MinIO (replaces PVC binary-writer pods) ──
- |
if [ -d "build-out/services" ] || [ -d "build-out/training" ] || [ -d "web-dashboard/dist" ]; then
set -e
echo "=== Uploading binaries to MinIO ==="
export AWS_ACCESS_KEY_ID="${MINIO_ACCESS_KEY}"
export AWS_SECRET_ACCESS_KEY="${MINIO_SECRET_KEY}"
S3="aws s3 --endpoint-url https://minio.foxhunt.svc.cluster.local:9000 --no-verify-ssl"
if [ -d "build-out/services" ]; then
$S3 cp build-out/services/ s3://foxhunt-binaries/services/ --recursive
echo "Service binaries uploaded to MinIO"
fi
if [ -d "build-out/training" ]; then
$S3 cp build-out/training/ s3://foxhunt-binaries/training/ --recursive
echo "Training binaries uploaded to MinIO"
fi
if [ -d "web-dashboard/dist" ]; then
$S3 cp web-dashboard/dist/ s3://foxhunt-binaries/static/ --recursive
echo "Web dashboard assets uploaded to MinIO"
fi
echo "=== MinIO upload complete ==="
else
echo "No new binaries — compile jobs did not run"
fi
```
**Step 3: Verify YAML syntax**
Run: `python3 -c "import yaml; yaml.safe_load(open('.gitlab-ci.yml'))" && echo OK`
Expected: `OK`
**Step 4: Commit**
```bash
git add .gitlab-ci.yml
git commit -m "refactor(ci): replace PVC binary-writers with MinIO upload
Deploy job now uploads binaries to MinIO via aws-cli (already in
infra-runner image). Eliminates binary-writer and training-binary-writer
pods, removing the L40S autoscale trigger for PVC writes."
```
---
### Task 2: Update trading-service.yaml — PVC to initContainer+emptyDir
**Files:**
- Modify: `infra/k8s/services/trading-service.yaml`
**Step 1: Add initContainer block**
Insert after `nodeSelector:` / before `containers:` (between lines 39 and 40):
```yaml
initContainers:
- name: fetch-binary
image: rg.fr-par.scw.cloud/foxhunt-ci/foxhunt-runtime:latest
command: ["/bin/sh", "-c"]
args:
- |
set -e
rclone copyto \
":s3:foxhunt-binaries/services/trading_service" \
"/binaries/trading_service" \
--s3-provider=Minio \
--s3-endpoint=https://minio.foxhunt.svc.cluster.local:9000 \
--s3-access-key-id="${MINIO_ACCESS_KEY}" \
--s3-secret-access-key="${MINIO_SECRET_KEY}" \
--s3-no-check-bucket \
--no-check-certificate
chmod +x /binaries/trading_service
echo "Fetched trading_service ($(stat -c%s /binaries/trading_service) bytes)"
env:
- name: MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: minio-credentials
key: access-key
- name: MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: minio-credentials
key: secret-key
volumeMounts:
- name: binaries
mountPath: /binaries
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
```
**Step 2: Replace PVC volume with emptyDir**
Replace:
```yaml
- name: binaries
persistentVolumeClaim:
claimName: foxhunt-binaries
readOnly: true
```
With:
```yaml
- name: binaries
emptyDir:
sizeLimit: 200Mi
```
**Step 3: Remove readOnly from main container's binaries volumeMount**
The main container `volumeMounts` has `readOnly: true` on the binaries mount. The initContainer needs write access. Change:
```yaml
- name: binaries
mountPath: /binaries
readOnly: true
```
To:
```yaml
- name: binaries
mountPath: /binaries
readOnly: true
```
Actually — the initContainer writes, the main container reads. emptyDir supports this: the initContainer writes, then terminates, then the main container mounts the same emptyDir read-only. Keep `readOnly: true` on the main container's mount. The initContainer's mount does NOT have `readOnly: true`.
**Step 4: Verify YAML**
Run: `python3 -c "import yaml; list(yaml.safe_load_all(open('infra/k8s/services/trading-service.yaml')))" && echo OK`
Expected: `OK`
---
### Task 3: Update api-gateway.yaml
**Files:**
- Modify: `infra/k8s/services/api-gateway.yaml`
Same pattern as Task 2. Binary name: `api_gateway`
**Step 1: Add initContainer** (between `nodeSelector:` and `containers:`, after line 40)
Same initContainer as Task 2 but with binary name `api_gateway`:
- `rclone copyto ":s3:foxhunt-binaries/services/api_gateway" "/binaries/api_gateway"`
- `chmod +x /binaries/api_gateway`
- `echo "Fetched api_gateway ..."`
**Step 2: Replace PVC volume with emptyDir** (lines 122-126)
Replace:
```yaml
- name: binaries
persistentVolumeClaim:
claimName: foxhunt-binaries
readOnly: true
```
With:
```yaml
- name: binaries
emptyDir:
sizeLimit: 200Mi
```
---
### Task 4: Update broker-gateway.yaml
**Files:**
- Modify: `infra/k8s/services/broker-gateway.yaml`
Same pattern. Binary name: `broker_gateway_service`
**Step 1: Add initContainer** (between `nodeSelector:` line 39 and `containers:` line 40)
Binary: `broker_gateway_service`
**Step 2: Replace PVC volume with emptyDir** (lines 116-120)
---
### Task 5: Update ml-training-service.yaml
**Files:**
- Modify: `infra/k8s/services/ml-training-service.yaml`
Binary name: `ml_training_service`. This service already has `minio-credentials` env vars and `minio-ca` volume.
**Step 1: Add initContainer** (between `nodeSelector:` line 82 and `containers:` line 83)
Same pattern. Binary: `ml_training_service`. Command includes `"serve"` arg — that's on the main container, not the initContainer.
**Step 2: Replace PVC volume with emptyDir** (lines 166-170)
Replace:
```yaml
- name: binaries
persistentVolumeClaim:
claimName: foxhunt-binaries
readOnly: true
```
With:
```yaml
- name: binaries
emptyDir:
sizeLimit: 200Mi
```
---
### Task 6: Update backtesting-service.yaml
**Files:**
- Modify: `infra/k8s/services/backtesting-service.yaml`
Binary name: `backtesting_service`
**Step 1: Add initContainer** (between `nodeSelector:` line 39 and `containers:` line 40)
**Step 2: Replace PVC volume with emptyDir** (lines 105-109)
---
### Task 7: Update trading-agent-service.yaml
**Files:**
- Modify: `infra/k8s/services/trading-agent-service.yaml`
Binary name: `trading_agent_service`
**Step 1: Add initContainer** (between `nodeSelector:` line 39 and `containers:` line 40)
**Step 2: Replace PVC volume with emptyDir** (lines 103-107)
---
### Task 8: Update data-acquisition-service.yaml
**Files:**
- Modify: `infra/k8s/services/data-acquisition-service.yaml`
Binary name: `data_acquisition_service`
**Step 1: Add initContainer** (between `nodeSelector:` line 39 and `containers:` line 40)
**Step 2: Replace PVC volume with emptyDir** (lines 96-100)
---
### Task 9: Update web-gateway.yaml (special — needs static assets too)
**Files:**
- Modify: `infra/k8s/services/web-gateway.yaml`
Binary name: `web-gateway`. Additionally fetches static dashboard assets.
**Step 1: Add initContainer with dual fetch**
Insert between `nodeSelector:` line 39 and `containers:` line 40:
```yaml
initContainers:
- name: fetch-binary
image: rg.fr-par.scw.cloud/foxhunt-ci/foxhunt-runtime:latest
command: ["/bin/sh", "-c"]
args:
- |
set -e
RCLONE_FLAGS="--s3-provider=Minio --s3-endpoint=https://minio.foxhunt.svc.cluster.local:9000 --s3-access-key-id=${MINIO_ACCESS_KEY} --s3-secret-access-key=${MINIO_SECRET_KEY} --s3-no-check-bucket --no-check-certificate"
rclone copyto ":s3:foxhunt-binaries/services/web-gateway" "/binaries/web-gateway" $RCLONE_FLAGS
chmod +x /binaries/web-gateway
echo "Fetched web-gateway ($(stat -c%s /binaries/web-gateway) bytes)"
rclone copy ":s3:foxhunt-binaries/static/" "/binaries/static/" $RCLONE_FLAGS
echo "Fetched static assets"
env:
- name: MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: minio-credentials
key: access-key
- name: MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: minio-credentials
key: secret-key
volumeMounts:
- name: binaries
mountPath: /binaries
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
```
Note: `sizeLimit` on the emptyDir should be `500Mi` for web-gateway (binary + static assets).
**Step 2: Replace PVC volume with emptyDir** (lines 102-106)
Replace:
```yaml
- name: binaries
persistentVolumeClaim:
claimName: foxhunt-binaries
readOnly: true
```
With:
```yaml
- name: binaries
emptyDir:
sizeLimit: 500Mi
```
---
### Task 10: Commit all service yaml changes
```bash
git add infra/k8s/services/
git commit -m "refactor(k8s): replace PVC mounts with MinIO rclone initContainers
All 8 service deployments now fetch their binary from MinIO at pod
startup via rclone initContainer into emptyDir. Eliminates dependency
on foxhunt-binaries PVC (RWO node affinity constraint).
web-gateway additionally fetches static dashboard assets."
```
---
### Task 11: Update training job-template.yaml
**Files:**
- Modify: `infra/k8s/training/job-template.yaml`
**Step 1: Replace existing fetch-binary initContainer**
The current initContainer (lines 41-73) copies from `training-binaries` PVC. Replace entire initContainer with:
```yaml
initContainers:
- name: fetch-binary
image: rg.fr-par.scw.cloud/foxhunt-ci/foxhunt-training-runtime:latest
command: ["/bin/sh", "-c"]
args:
- |
set -e
rclone copyto \
":s3:foxhunt-binaries/training/$(TRAINING_BINARY)" \
"/binaries/$(TRAINING_BINARY)" \
--s3-provider=Minio \
--s3-endpoint=https://minio.foxhunt.svc.cluster.local:9000 \
--s3-access-key-id="${MINIO_ACCESS_KEY}" \
--s3-secret-access-key="${MINIO_SECRET_KEY}" \
--s3-no-check-bucket \
--no-check-certificate
chmod +x "/binaries/$(TRAINING_BINARY)"
echo "Fetched $(TRAINING_BINARY) ($(stat -c%s /binaries/$(TRAINING_BINARY)) bytes)"
env:
- name: TRAINING_BINARY
value: train_baseline_supervised
- name: MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: minio-credentials
key: access-key
- name: MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: minio-credentials
key: secret-key
volumeMounts:
- name: binaries
mountPath: /binaries
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
```
**Step 2: Remove training-binaries-src volume**
Delete from volumes section (lines 122-127):
```yaml
- name: training-binaries-src
persistentVolumeClaim:
claimName: training-binaries
readOnly: true
```
**Step 3: Verify YAML**
Run: `python3 -c "import yaml; list(yaml.safe_load_all(open('infra/k8s/training/job-template.yaml')))" && echo OK`
Expected: `OK`
**Step 4: Commit**
```bash
git add infra/k8s/training/job-template.yaml
git commit -m "refactor(k8s): training job fetches binary from MinIO
Replace PVC-based training binary distribution with MinIO rclone
initContainer. Training jobs no longer need training-binaries PVC,
eliminating L40S GPU node autoscale just for binary distribution."
```
---
### Task 12: Delete PVC manifests
**Files:**
- Delete: `infra/k8s/storage/foxhunt-binaries-pvc.yaml`
- Delete: `infra/k8s/training/training-binaries-pvc.yaml`
**Step 1: Delete the files**
```bash
rm infra/k8s/storage/foxhunt-binaries-pvc.yaml
rm infra/k8s/training/training-binaries-pvc.yaml
```
**Step 2: Verify no remaining references to deleted PVCs**
Run: `grep -r "foxhunt-binaries" infra/ .gitlab-ci.yml --include='*.yaml' --include='*.yml'`
Expected: Only MinIO bucket references (`s3://foxhunt-binaries/`), no PVC references.
Run: `grep -r "training-binaries" infra/ .gitlab-ci.yml --include='*.yaml' --include='*.yml'`
Expected: No results (the PVC name is gone).
**Step 3: Commit**
```bash
git add -A infra/k8s/storage/foxhunt-binaries-pvc.yaml infra/k8s/training/training-binaries-pvc.yaml
git commit -m "chore(k8s): delete foxhunt-binaries and training-binaries PVCs
These PVCs are replaced by MinIO-based binary distribution.
Service pods and training jobs now fetch binaries from MinIO
via rclone initContainers.
NOTE: Run 'kubectl delete pvc foxhunt-binaries training-binaries -n foxhunt'
on the cluster to reclaim the block storage volumes."
```
---
### Task 13: Final verification
**Step 1: Grep for any remaining PVC binary references**
Run: `grep -rn "claimName: foxhunt-binaries\|claimName: training-binaries" infra/ .gitlab-ci.yml`
Expected: No results.
**Step 2: Grep for binary-writer references**
Run: `grep -rn "binary-writer\|binary_writer" .gitlab-ci.yml`
Expected: No results.
**Step 3: Verify all service yamls have initContainers**
Run: `grep -l "fetch-binary" infra/k8s/services/*.yaml | wc -l`
Expected: `8`
**Step 4: Verify training job has MinIO fetch**
Run: `grep "foxhunt-binaries/training" infra/k8s/training/job-template.yaml`
Expected: Match on the rclone line.
---
## Summary of Changes
| File | Action | Lines ~Δ |
|------|--------|----------|
| `.gitlab-ci.yml` | Replace binary-writer pods with `aws s3 cp` | -100/+20 |
| `infra/k8s/services/trading-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/api-gateway.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/broker-gateway.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/ml-training-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/backtesting-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/trading-agent-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/data-acquisition-service.yaml` | Add initContainer, PVC→emptyDir | +30/-4 |
| `infra/k8s/services/web-gateway.yaml` | Add initContainer (binary+static), PVC→emptyDir | +35/-4 |
| `infra/k8s/training/job-template.yaml` | Replace PVC initContainer with MinIO | +30/-20 |
| `infra/k8s/storage/foxhunt-binaries-pvc.yaml` | Delete | -19 |
| `infra/k8s/training/training-binaries-pvc.yaml` | Delete | -19 |
**Total: 12 files, ~+275/-210 lines, 0 image rebuilds**
## Post-Deploy Cluster Cleanup
After deploying this change, manually delete the orphaned PVCs:
```bash
kubectl delete pvc foxhunt-binaries training-binaries -n foxhunt
```
Add `MINIO_ACCESS_KEY` and `MINIO_SECRET_KEY` as CI/CD variables in GitLab
(Settings → CI/CD → Variables), or source them from the cluster secret during the deploy job.