Allt i Kubernetes: containrar, manifest, orkestertjänst och CI

Målarkitekturen ur Master Prompt som kod:

- Webben containeriserad (flerstegs-Dockerfile → oprivilegierad nginx
  med SPA-fallback, hashade assets cachas hårt, säkerhetsheaders).
- AI-orkestern som egen tjänst (services/ai-orkester): samma routing
  och AI-regler som edge-funktionen men körbar som pod — egen
  HS256-JWT-verifiering (fail closed), /halsa, storleksgränser,
  read-only container som kör som node-användaren. Livetestad:
  hälsokontroll ok, 401 utan/med ogiltig token, 400-validering bakom
  giltig JWT.
- Kubernetes-manifest (infra/k8s, kustomize): namespace, Deployments
  med readiness/liveness och åtstramad securityContext, Services,
  HPA 2–10 pods på 70 % CPU, PodDisruptionBudgets, Ingress med TLS
  via cert-manager, secret-mall (riktiga värden skapas utanför git).
- Klienten väljer AI-väg vid bygget: VITE_AI_ORKESTER_URL → tjänsten
  i klustret, annars Supabase-edge-funktionen. Samma orkester i båda,
  låst av paritetstest.
- CI-arbetsflöde: tester + produktionsbygge + verifierande
  containerbyggen av båda bilderna på varje push/PR.
- docs/DRIFT.md: arkitekturdiagram, driftsättningssteg, skalning,
  och vad som återstår mot full självhostning.

Verifierat: 27 vitest-tester gröna, produktionsbygge ok, alla
K8s-manifest YAML-validerade, orkestertjänsten livetestad,
Playwright-röktest grönt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-03 01:27:54 +00:00
parent 8b79e3dfd5
commit 03f1ef20f6
16 changed files with 719 additions and 4 deletions
+80
View File
@@ -0,0 +1,80 @@
# AI-orkestern: plattformens AI-endpoint. Claude API-nyckeln och
# JWT-hemligheten kommer från secret:en felsokning-hemligheter —
# de finns aldrig i bilder, manifest eller klient.
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-orkester
namespace: guidad-felsokning
labels: { app: ai-orkester }
spec:
replicas: 2
selector:
matchLabels: { app: ai-orkester }
template:
metadata:
labels: { app: ai-orkester }
spec:
securityContext:
runAsNonRoot: true
containers:
- name: ai-orkester
image: ghcr.io/ORGANISATION/guidad-felsokning-ai-orkester:latest # ersätt med ert registry
ports:
- containerPort: 8080
env:
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef: { name: felsokning-hemligheter, key: anthropic-api-key }
- name: SUPABASE_JWT_SECRET
valueFrom:
secretKeyRef: { name: felsokning-hemligheter, key: supabase-jwt-secret }
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: "1", memory: 256Mi }
readinessProbe:
httpGet: { path: /halsa, port: 8080 }
initialDelaySeconds: 3
livenessProbe:
httpGet: { path: /halsa, port: 8080 }
periodSeconds: 15
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
---
apiVersion: v1
kind: Service
metadata:
name: ai-orkester
namespace: guidad-felsokning
spec:
selector: { app: ai-orkester }
ports:
- port: 80
targetPort: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ai-orkester
namespace: guidad-felsokning
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: ai-orkester }
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ai-orkester
namespace: guidad-felsokning
spec:
minAvailable: 1
selector:
matchLabels: { app: ai-orkester }
+17
View File
@@ -0,0 +1,17 @@
# EXEMPEL — checka aldrig in riktiga värden. Skapa hellre direkt:
#
# kubectl -n guidad-felsokning create secret generic felsokning-hemligheter \
# --from-literal=anthropic-api-key='sk-ant-…' \
# --from-literal=supabase-jwt-secret='…'
#
# I produktion: använd extern secrets-hantering (External Secrets Operator,
# Sealed Secrets eller molnleverantörens secret manager).
apiVersion: v1
kind: Secret
metadata:
name: felsokning-hemligheter
namespace: guidad-felsokning
type: Opaque
stringData:
anthropic-api-key: ERSATT-MIG
supabase-jwt-secret: ERSATT-MIG
+29
View File
@@ -0,0 +1,29 @@
# Ingress med TLS (cert-manager): webben på /, AI-orkestern på /api/ai.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: guidad-felsokning
namespace: guidad-felsokning
annotations:
cert-manager.io/cluster-issuer: letsencrypt # ersätt med er issuer
spec:
ingressClassName: nginx
tls:
- hosts: [app.exempel.se] # ersätt med er domän
secretName: guidad-felsokning-tls
rules:
- host: app.exempel.se # ersätt med er domän
http:
paths:
- path: /api/ai
pathType: Prefix
backend:
service: { name: ai-orkester, port: { number: 80 } }
- path: /halsa
pathType: Prefix
backend:
service: { name: ai-orkester, port: { number: 80 } }
- path: /
pathType: Prefix
backend:
service: { name: web, port: { number: 80 } }
+9
View File
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- web.yaml
- ai-orkester.yaml
- ingress.yaml
# hemligheter.exempel.yaml appliceras medvetet inte — skapa secret:en
# separat (se kommentaren i den filen).
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: guidad-felsokning
+70
View File
@@ -0,0 +1,70 @@
# Webbklienten: statisk SPA bakom oprivilegierad nginx.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: guidad-felsokning
labels: { app: web }
spec:
replicas: 2
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
securityContext:
runAsNonRoot: true
containers:
- name: web
image: ghcr.io/ORGANISATION/guidad-felsokning-web:latest # ersätt med ert registry
ports:
- containerPort: 8080
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 250m, memory: 128Mi }
readinessProbe:
httpGet: { path: /, port: 8080 }
initialDelaySeconds: 3
livenessProbe:
httpGet: { path: /, port: 8080 }
periodSeconds: 15
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: [ALL] }
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: guidad-felsokning
spec:
selector: { app: web }
ports:
- port: 80
targetPort: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
namespace: guidad-felsokning
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: web }
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web
namespace: guidad-felsokning
spec:
minAvailable: 1
selector:
matchLabels: { app: web }