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

17 KiB

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):

    - 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):

    # ── 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

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):

      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:

        - name: binaries
          persistentVolumeClaim:
            claimName: foxhunt-binaries
            readOnly: true

With:

        - 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:

            - name: binaries
              mountPath: /binaries
              readOnly: true

To:

            - 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:

        - name: binaries
          persistentVolumeClaim:
            claimName: foxhunt-binaries
            readOnly: true

With:

        - 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:

        - name: binaries
          persistentVolumeClaim:
            claimName: foxhunt-binaries
            readOnly: true

With:

        - 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:

      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:

        - name: binaries
          persistentVolumeClaim:
            claimName: foxhunt-binaries
            readOnly: true

With:

        - name: binaries
          emptyDir:
            sizeLimit: 500Mi

Task 10: Commit all service yaml changes

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:

      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):

        - 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

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

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

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:

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.