Files
woggioni e0896b4a95
CI / Build and push docker image (push) Successful in 3m24s
Configure CORS headers from environment variables
2026-09-18 19:18:27 +08:00

302 lines
8.9 KiB
YAML

# 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
# Postgres (external, lives in another namespace): everything except the
# password, which is the only entry in the tavolo-secrets Secret.
# Use its Service DNS name, e.g. postgres.<namespace>.svc.cluster.local.
DATABASE_ENGINE: postgres
DATABASE_HOST: REPLACE_ME
DATABASE_PORT: "5432"
DATABASE_NAME: REPLACE_ME
DATABASE_USER: REPLACE_ME
# Extra DSN query parameters appended to the URL (e.g. ssl=require).
# Empty means none.
DATABASE_OPTIONS: ""
# 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"
# CORS (kaya-cors' CorsMixin). Disabled unless CORS_ALLOW_ORIGINS or
# CORS_ALLOW_ORIGIN_REGEX is set — unneeded when the SPA and the API are
# served from the same origin. See server/.env.example for details.
# CORS_ALLOW_ORIGINS: "https://example.com,https://app.example.com" # or "*"
# CORS_ALLOW_ORIGIN_REGEX: 'https://tavolo-[a-z0-9-]+\.vercel\.app'
# CORS_ALLOW_METHODS: "GET,POST" # default: GET; "*" = all
# CORS_ALLOW_HEADERS: "Authorization,Content-Type" # "*" mirrors the request
# CORS_ALLOW_CREDENTIALS: "false"
# CORS_EXPOSE_HEADERS: ""
# CORS_MAX_AGE: "600"
# 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://<host>:<port>/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:
# The only Postgres secret: the password for DATABASE_USER at
# DATABASE_HOST (both configured in the tavolo-config ConfigMap).
DATABASE_PASSWORD: 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:
# Migrations need the non-secret DATABASE_* parts too.
- configMapRef:
name: tavolo-config
- 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