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
278 lines
7.7 KiB
YAML
278 lines
7.7 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
|
|
# 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://<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:
|
|
# Postgres lives in another namespace; use its Service DNS name:
|
|
# postgres://USER:PASS@postgres.<namespace>.svc.cluster.local:5432/<db>
|
|
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
|