From f6239d26378f5b551e407cdbe95db069541683ff Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Thu, 17 Sep 2026 08:26:42 +0800 Subject: [PATCH] Add Kubernetes manifest for the tavolo namespace Single-file deployment under deploy/k8s/tavolo.yaml: - Namespace, ConfigMap and Secret (placeholders) for the app - Ephemeral Redis Deployment + Service (sessions and live games are disposable, mirroring the docker-compose no-volume choice) - App Deployment with an aerich-migrate initContainer so the schema is upgraded before rollout, /api/health probes and a hardened pod security context - ClusterIP Service; Postgres and the OIDC provider stay in their own namespaces and are referenced by DNS --- deploy/k8s/tavolo.yaml | 277 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 deploy/k8s/tavolo.yaml diff --git a/deploy/k8s/tavolo.yaml b/deploy/k8s/tavolo.yaml new file mode 100644 index 0000000..a580c4d --- /dev/null +++ b/deploy/k8s/tavolo.yaml @@ -0,0 +1,277 @@ +# Tavolo — Kubernetes deployment (namespace: tavolo) +# +# Single apply: +# kubectl apply -f deploy/k8s/tavolo.yaml +# +# Replace every value marked REPLACE_ME before applying. +# +# Scope: +# - Postgres and the OIDC provider are NOT part of this file; they are +# deployed in other namespaces and referenced by DNS name. +# - Redis IS included (ephemeral: sessions and live games are disposable, +# mirroring docker-compose's no-volume choice). +# +# Reachability notes: +# - The OIDC issuer URL must resolve from the browser AND from the pods +# (kaya does discovery lazily on the first login, so startup succeeds +# even when the issuer is unreachable, but logins then fail). If the +# provider is internal-only, you need split DNS or a public issuer URL. +# - The Postgres NetworkPolicy (if any) must allow ingress from the +# tavolo namespace. +# - This file deploys a ClusterIP Service only. Until you add an Ingress, +# reach the app with: +# kubectl port-forward -n tavolo svc/tavolo 8080:80 +# and set OIDC_REDIRECT_URI to match whatever URL the browser uses. + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: tavolo + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/part-of: tavolo + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: tavolo-config + namespace: tavolo + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: app + app.kubernetes.io/part-of: tavolo +data: + # In-cluster Redis deployed by this file. + REDIS_URL: redis://redis.tavolo.svc.cluster.local:6379/0 + # The image bakes STATIC_DIR=/app/web/dist; repeat it here for clarity. + STATIC_DIR: /app/web/dist + GAME_TTL_SECONDS: "86400" + HAND_ACK_TIMEOUT_SECONDS: "30" + TURN_TIMEOUT_SECONDS: "30" + # OIDC (provider lives in another namespace). + OIDC_CLIENT_ID: tavolo + OIDC_POST_LOGIN_REDIRECT: / + OIDC_POST_LOGOUT_REDIRECT: / + # Issuer URL as seen by the browser AND the pods (see notes above). + OIDC_ISSUER: REPLACE_ME + # Public callback URL of this deployment, e.g. + # http://:/auth/callback (must be allowed at the provider). + OIDC_REDIRECT_URI: REPLACE_ME + +--- +apiVersion: v1 +kind: Secret +metadata: + name: tavolo-secrets + namespace: tavolo + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: app + app.kubernetes.io/part-of: tavolo +type: Opaque +stringData: + # Postgres lives in another namespace; use its Service DNS name: + # postgres://USER:PASS@postgres..svc.cluster.local:5432/ + DATABASE_URL: REPLACE_ME + # Client secret for OIDC_CLIENT_ID at the provider. + OIDC_CLIENT_SECRET: REPLACE_ME + +--- +# Redis: sessions + live game state. Ephemeral by design (emptyDir): worst +# case after a restart users log in again and games in progress expire. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: tavolo + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: redis + app.kubernetes.io/part-of: tavolo +spec: + replicas: 1 + # Redis is a singleton here; avoid a window with two masters on rollout. + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: redis + template: + metadata: + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: redis + app.kubernetes.io/part-of: tavolo + spec: + securityContext: + runAsNonRoot: true + runAsUser: 999 # redis user in the official image + fsGroup: 999 + containers: + - name: redis + image: redis:8-alpine + # Snapshots would only live on an emptyDir anyway: skip them. + args: ["--save", "", "--appendonly", "no"] + ports: + - name: redis + containerPort: 6379 + volumeMounts: + - name: data + mountPath: /data + readinessProbe: + exec: + command: ["redis-cli", "ping"] + periodSeconds: 5 + livenessProbe: + exec: + command: ["redis-cli", "ping"] + periodSeconds: 10 + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 250m + memory: 128Mi + volumes: + - name: data + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: tavolo + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: redis + app.kubernetes.io/part-of: tavolo +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: redis + ports: + - name: redis + port: 6379 + targetPort: redis + +--- +# The app (backend + compiled SPA + WebSocket endpoint, one image). +# The "migrate" initContainer runs `aerich upgrade` before the app starts, +# so this single file can be applied safely in any order. Migrations are +# idempotent, but if you ever scale above 1 replica, move this to a Job to +# avoid concurrent upgrades. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tavolo + namespace: tavolo + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: app + app.kubernetes.io/part-of: tavolo +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: app + template: + metadata: + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: app + app.kubernetes.io/part-of: tavolo + spec: + # Let WebSocket connections drain on rollout. + terminationGracePeriodSeconds: 30 + securityContext: + runAsNonRoot: true + initContainers: + - name: migrate + image: gitea.woggioni.net/woggioni/tavolo:latest + # aerich reads [tool.aerich] from pyproject.toml in /app. + command: ["aerich", "upgrade"] + workingDir: /app + envFrom: + - secretRef: + name: tavolo-secrets + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + containers: + - name: tavolo + # Pin a release/* tag for production instead of :latest. + image: gitea.woggioni.net/woggioni/tavolo:latest + ports: + - name: http + containerPort: 8080 + envFrom: + - configMapRef: + name: tavolo-config + - secretRef: + name: tavolo-secrets + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + volumeMounts: + - name: tmp + mountPath: /tmp + startupProbe: + httpGet: + path: /api/health + port: http + periodSeconds: 2 + failureThreshold: 30 + readinessProbe: + httpGet: + path: /api/health + port: http + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/health + port: http + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumes: + - name: tmp + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: tavolo + namespace: tavolo + labels: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: app + app.kubernetes.io/part-of: tavolo +spec: + type: ClusterIP + # No session affinity needed: sessions and live games live in Redis. + selector: + app.kubernetes.io/name: tavolo + app.kubernetes.io/component: app + ports: + - name: http + port: 80 + targetPort: http