herzogflorian c057f8efb7 Kubernetes deployment: shared data directory, write lock, probes
A third deployment next to the two on the DGX Spark, which stay as they
are: deploy/k8s/ has its own image and Kustomize manifests. The API is a
Deployment with probes, the data a PersistentVolumeClaim, the nightly
update a CronJob; overlays/gpu adds cuOpt on a GPU of the cluster.

Five assumptions of the single-container setup did not hold for pods, so
the service changed (on the Spark nothing changes that one would notice):

- probes: GET /health/live (async, no I/O) and GET /health/ready (data
  directory readable); neither needs cuOpt or market data, so a cuOpt
  outage never gets the API restarted. /health is unchanged
- DataStore replaces every file atomically (temporary file renamed into
  place) and saves the manifest last; the API reloads its market data
  when the manifest changes, so it serves what a CronJob wrote
- one writer at a time across processes and pods: an flock on
  /data/.write.lock, taken by API jobs and the CLI's data commands
  (setup, reselect, update, clean, snapshot); a second writer gets 409 or
  exit status 3, with the holder's name. `clean` now updates the manifest
- job records live in /data/jobs/ and are written at every status
  change: every replica answers GET /jobs/{id}, and a job whose process
  has gone is reported failed ("interrupted")
- the image runs as uid 10001 under a read-only root file system, with
  the library versions pinned to deploy/apptainer/constraints.txt (SciPy
  1.18 releases the GIL while HiGHS solves, so the liveness probe is
  answered during an optimisation)

docs/KUBERNETES.md is the guide (build, deploy, load the data, operate,
troubleshooting); README, USAGE and ARCHITECTURE point to it.

Both kustomizations render (kubectl kustomize); the k3s test of the
deployment is recorded in docs/KUBERNETES.md. 149 tests (148 pass, the
cuOpt round trip skipped without a server).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:27:43 +01:00

98 lines
3.0 KiB
YAML

# The API (and the test bench at /ui/): uvicorn with one worker per pod; scale with replicas.
#
# Replicas share the data volume (see pvc.yaml): job records and the write lock live in it, and each
# pod reloads the market data when a data job has written new data. One replica is the default.
apiVersion: apps/v1
kind: Deployment
metadata:
name: quantcore
labels:
app.kubernetes.io/name: quantcore
app.kubernetes.io/component: api
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # the new pod is ready before the old one stops: no downtime on a rollout
selector:
matchLabels:
app.kubernetes.io/name: quantcore
app.kubernetes.io/component: api
template:
metadata:
labels:
app.kubernetes.io/name: quantcore
app.kubernetes.io/component: api
spec:
# SIGTERM lets uvicorn finish the requests in flight: an optimisation can take the whole
# QUANTCORE_CUOPT_TIME_LIMIT (300 s) before it answers
terminationGracePeriodSeconds: 360
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001 # makes the data volume writable for that user
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: quantcore
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 5001
envFrom:
- configMapRef:
name: quantcore-config
resources:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "4" # = the BLAS threads of quantcore.env
memory: 4Gi
# live: the process answers (no I/O, no cuOpt); ready: the data volume can be read
startupProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 2
failureThreshold: 60
livenessProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: http
periodSeconds: 10
timeoutSeconds: 5
lifecycle:
preStop:
exec:
command: ["sleep", "5"] # the Service stops sending requests before uvicorn stops
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: quantcore-data
- name: tmp
emptyDir:
sizeLimit: 1Gi