Terraform blir enda vägen, och backup ett obligatoriskt val
Kustomize- och Argo CD-vägen är borttagen. Den beskrev samma system en
gång till och kunde inte köras samtidigt som Terraform utan att de
motarbetade varandra — selfHeal återställde det Terraform ändrade och
prune tog bort det Terraform skapade. Kvar utanför Terraform är bara
postgres-init.sql, som läses av både definitionen och integrationstestet
så att schemat inte kan glida isär från det som testas.
Säkerhetskopieringen var en förhoppning: en StatefulSet med en volym och
ingen kopia. Går volymen förlorad är det inte "data" som försvinner utan
varje ärendes bevisvärde — vad som kontrollerades, av vem, när, med
vilken evidens — och det går inte att återskapa i efterhand.
databas_lage är därför ett obligatoriskt val utan standardvärde:
extern managerad Postgres, leverantörens backup och PITR
(rekommenderat i produktion)
cnpg CloudNativePG i klustret: basbackup 02:30, kontinuerlig
WAL-arkivering till objektlagring, PITR och failover
inbyggd en volym, ingen backup — spärras av en precondition när
miljön är produktion
Preconditions fångar felkonfiguration vid plan i stället för vid drift:
extern utan anslutning, cnpg utan backupmål eller nycklar, inbyggd i
produktion.
Driftsättningen är nu två åtskilda flöden. Publicera bygger och taggar
bilderna vid varje main-push; Driftsätt startas för hand med en tagg mot
en GitHub-miljö som kan kräva godkännande, kör fmt/init/validate/plan/
apply, skriver ut kartan och rökkontrollerar hälsa och API-spec. En bild
i registret är inte samma sak som en bild som kör. Rollback är att köra
Driftsätt igen med en tidigare tagg. CI kör dessutom terraform validate
på varje PR — den kontroll jag inte kunde köra själv.
Två fel hittade vid egengranskning av definitionen: schemafilen delades
på semikolon, vilket hade klippt itu plpgsql-funktionen med
append-only-triggern (nu hela filen via postInitApplicationSQLRefs), och
null-satta fält i kubernetes_manifest utelämnas nu i stället.
Verifierat: 87 vitest-tester, typkontroll, eslint, OpenAPI-validering,
terraform fmt, statisk referenskontroll av modulen och integrationstest
mot riktig Postgres. terraform validate kunde inte köras här —
registry.terraform.io är blockerad av sessionens egress-policy, därav
CI-jobbet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
# Argo CD-applikationen: klustret följer git — appliceras EN gång i
|
||||
# argocd-namespacet, därefter sköter Argo CD all synk automatiskt.
|
||||
#
|
||||
# kubectl apply -f infra/gitops/argocd-application.yaml
|
||||
#
|
||||
# prune tar bort resurser som försvunnit ur git; selfHeal återställer
|
||||
# manuella ändringar i klustret. Hemligheten felsokning-hemligheter
|
||||
# ligger medvetet utanför git och utanför synken.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: guidad-felsokning
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: https://github.com/ERSATT-MIG/ERSATT-MIG.git # ersätt med ert repo
|
||||
targetRevision: main
|
||||
path: infra/overlays/produktion
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- ServerSideApply=true
|
||||
@@ -1,80 +0,0 @@
|
||||
# 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: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef: { name: felsokning-hemligheter, key: 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 }
|
||||
@@ -1,24 +0,0 @@
|
||||
# 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=jwt-secret="$(openssl rand -base64 48)" \
|
||||
# --from-literal=postgres-losenord="$(openssl rand -base64 24)" \
|
||||
# --from-literal=integration-nyckel="$(openssl rand -hex 32)"
|
||||
#
|
||||
# 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
|
||||
jwt-secret: ERSATT-MIG
|
||||
postgres-losenord: ERSATT-MIG
|
||||
# 32 byte hex (openssl rand -hex 32) — krypterar kundernas
|
||||
# märkesspecifika credentials i vila. Byts nyckeln måste kopplingarna
|
||||
# sparas om; tjänsten visar då inga värden i stället för att gissa.
|
||||
integration-nyckel: ERSATT-MIG
|
||||
@@ -1,39 +0,0 @@
|
||||
# Ingress med TLS (cert-manager). Längsta prefix vinner:
|
||||
# /api/ai → AI-orkestern, övriga /api + /halsa → plattformen, / → webben.
|
||||
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
|
||||
# Teknikern dokumenterar med foto och video som följer med
|
||||
# händelseloggen. Standardgränsen (1 MB) skulle avvisa
|
||||
# dokumentationen vid dörren medan tjänsten själv tar 4 MB.
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 8m
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "120" # AI-svar kan ta tid
|
||||
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: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service: { name: plattform, port: { number: 80 } }
|
||||
- path: /halsa
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service: { name: plattform, port: { number: 80 } }
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service: { name: web, port: { number: 80 } }
|
||||
@@ -1,17 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- postgres.yaml
|
||||
- plattform.yaml
|
||||
- web.yaml
|
||||
- ai-orkester.yaml
|
||||
- ingress.yaml
|
||||
# hemligheter.exempel.yaml appliceras medvetet inte — skapa secret:en
|
||||
# separat (se kommentaren i den filen).
|
||||
|
||||
configMapGenerator:
|
||||
- name: postgres-init
|
||||
namespace: guidad-felsokning
|
||||
files:
|
||||
- init.sql=postgres-init.sql
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: guidad-felsokning
|
||||
@@ -1,86 +0,0 @@
|
||||
# Plattformstjänsten: självhostad auth, händelse-API och Live Share-delning.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: plattform
|
||||
namespace: guidad-felsokning
|
||||
labels: { app: plattform }
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels: { app: plattform }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: plattform }
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
containers:
|
||||
- name: plattform
|
||||
image: ghcr.io/ORGANISATION/guidad-felsokning-plattform:latest # ersätt med ert registry
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef: { name: felsokning-hemligheter, key: jwt-secret }
|
||||
# Krypteringsnyckel för kundernas märkesspecifika credentials
|
||||
# (32 byte hex eller base64). Saknas den sparas inga uppgifter
|
||||
# alls — tjänsten failar closed i stället för att lagra i klartext.
|
||||
- name: INTEGRATION_NYCKEL
|
||||
valueFrom:
|
||||
secretKeyRef: { name: felsokning-hemligheter, key: integration-nyckel }
|
||||
- name: POSTGRES_LOSENORD
|
||||
valueFrom:
|
||||
secretKeyRef: { name: felsokning-hemligheter, key: postgres-losenord }
|
||||
- name: DATABASE_URL
|
||||
value: postgresql://plattform:$(POSTGRES_LOSENORD)@postgres:5432/felsokning
|
||||
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: plattform
|
||||
namespace: guidad-felsokning
|
||||
spec:
|
||||
selector: { app: plattform }
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: plattform
|
||||
namespace: guidad-felsokning
|
||||
spec:
|
||||
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: plattform }
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target: { type: Utilization, averageUtilization: 70 }
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: plattform
|
||||
namespace: guidad-felsokning
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels: { app: plattform }
|
||||
@@ -1,67 +0,0 @@
|
||||
# Självhostad Postgres för Guidad Felsökning.
|
||||
# Schemat (infra/k8s/postgres-init.sql) genereras in som ConfigMap av
|
||||
# kustomize och körs vid första starten — samma fil används av
|
||||
# integrationstestet. För produktion rekommenderas CloudNativePG-operatorn
|
||||
# (automatiska säkerhetskopior, failover, PITR).
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: guidad-felsokning
|
||||
spec:
|
||||
serviceName: postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels: { app: postgres }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: postgres }
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:17-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
value: felsokning
|
||||
- name: POSTGRES_USER
|
||||
value: plattform
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef: { name: felsokning-hemligheter, key: postgres-losenord }
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
- name: init
|
||||
mountPath: /docker-entrypoint-initdb.d
|
||||
resources:
|
||||
requests: { cpu: 250m, memory: 256Mi }
|
||||
limits: { cpu: "1", memory: 1Gi }
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "plattform", "-d", "felsokning"]
|
||||
initialDelaySeconds: 5
|
||||
volumes:
|
||||
- name: init
|
||||
configMap: { name: postgres-init }
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: guidad-felsokning
|
||||
spec:
|
||||
selector: { app: postgres }
|
||||
ports:
|
||||
- port: 5432
|
||||
@@ -1,70 +0,0 @@
|
||||
# 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 }
|
||||
@@ -1,19 +0,0 @@
|
||||
# Produktions-overlay — GitOps-sanningen för vad som kör i klustret.
|
||||
# Bildtaggarna uppdateras av publiceringsflödet (.github/workflows/
|
||||
# publicera.yml) med git-SHA:t för varje main-push; Argo CD synkar
|
||||
# klustret från den här katalogen. Ändra aldrig taggar för hand —
|
||||
# rulla tillbaka genom att revertera commiten.
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../k8s
|
||||
images:
|
||||
- name: ghcr.io/ORGANISATION/guidad-felsokning-web
|
||||
newName: ghcr.io/ersatt-mig/guidad-felsokning-web
|
||||
newTag: latest
|
||||
- name: ghcr.io/ORGANISATION/guidad-felsokning-ai-orkester
|
||||
newName: ghcr.io/ersatt-mig/guidad-felsokning-ai-orkester
|
||||
newTag: latest
|
||||
- name: ghcr.io/ORGANISATION/guidad-felsokning-plattform
|
||||
newName: ghcr.io/ersatt-mig/guidad-felsokning-plattform
|
||||
newTag: latest
|
||||
@@ -25,7 +25,12 @@ resource "kubernetes_secret_v1" "hemligheter" {
|
||||
|
||||
# Databasschemat körs vid databasens första start. Samma fil används av
|
||||
# integrationstestet, så schemat kan aldrig glida isär från det som testas.
|
||||
# I inbyggt läge körs schemat av postgres-bilden vid första starten, i
|
||||
# cnpg-läget av operatorn via postInitApplicationSQLRefs. I externt läge
|
||||
# ansvarar ni för att köra filen mot er databas.
|
||||
resource "kubernetes_config_map_v1" "postgres_init" {
|
||||
count = local.extern_databas ? 0 : 1
|
||||
|
||||
metadata {
|
||||
name = "postgres-init"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
@@ -33,6 +38,6 @@ resource "kubernetes_config_map_v1" "postgres_init" {
|
||||
}
|
||||
|
||||
data = {
|
||||
"init.sql" = file("${path.module}/../k8s/postgres-init.sql")
|
||||
"init.sql" = file("${path.module}/../postgres-init.sql")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,217 @@
|
||||
# Postgres — händelseloggen.
|
||||
# Händelseloggen — systemets enda sanningskälla.
|
||||
#
|
||||
# Append-only garanteras av triggers i schemat, inte av att API:t låter
|
||||
# bli att skriva. Det gör historiken oantastlig även för den som har
|
||||
# databasåtkomst med rätt roll.
|
||||
#
|
||||
# En StatefulSet med en PVC är INTE säkerhetskopiering. Innan skarp drift:
|
||||
# byt mot CloudNativePG-operatorn (basbackup + WAL-arkivering + PITR +
|
||||
# failover) och peka plattformens DATABASE_URL på dess tjänst.
|
||||
# Tre lägen, valda med var.databas_lage. Skillnaden mellan dem är
|
||||
# säkerhetskopiering, och det är därför variabeln saknar standardvärde:
|
||||
#
|
||||
# extern managerad Postgres — leverantören sköter backup och PITR
|
||||
# cnpg CloudNativePG — basbackup + WAL-arkivering + failover
|
||||
# inbyggd en volym, ingen backup — prov och demo
|
||||
#
|
||||
# Går loggen förlorad är det inte "data" som försvinner utan varje
|
||||
# ärendes bevisvärde: vad som kontrollerades, av vem, när, med vilken
|
||||
# evidens. Det går inte att återskapa i efterhand.
|
||||
|
||||
locals {
|
||||
inbyggd_databas = var.databas_lage == "inbyggd"
|
||||
cnpg_databas = var.databas_lage == "cnpg"
|
||||
extern_databas = var.databas_lage == "extern"
|
||||
|
||||
# Vart plattformstjänsten ansluter. CloudNativePG skapar en tjänst som
|
||||
# alltid pekar på den skrivbara instansen (-rw), så en failover kräver
|
||||
# ingen ändring här.
|
||||
databas_anslutning = (
|
||||
local.extern_databas
|
||||
? var.databas_url
|
||||
: local.cnpg_databas
|
||||
? "postgresql://plattform:$(POSTGRES_LOSENORD)@felsokning-db-rw:5432/felsokning"
|
||||
: "postgresql://plattform:$(POSTGRES_LOSENORD)@postgres:5432/felsokning"
|
||||
)
|
||||
}
|
||||
|
||||
# Fångar felkonfiguration vid plan i stället för vid drift.
|
||||
resource "terraform_data" "databaskontroll" {
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = !local.extern_databas || var.databas_url != ""
|
||||
error_message = "databas_lage = \"extern\" kräver databas_url."
|
||||
}
|
||||
|
||||
precondition {
|
||||
condition = !local.cnpg_databas || var.backup.mal != ""
|
||||
error_message = "databas_lage = \"cnpg\" kräver backup.mal — poängen med läget är säkerhetskopieringen."
|
||||
}
|
||||
|
||||
precondition {
|
||||
condition = !local.cnpg_databas || (var.backup_nyckel_id != "" && var.backup_nyckel != "")
|
||||
error_message = "databas_lage = \"cnpg\" kräver backup_nyckel_id och backup_nyckel."
|
||||
}
|
||||
|
||||
precondition {
|
||||
condition = !local.inbyggd_databas || var.miljo != "produktion"
|
||||
error_message = "databas_lage = \"inbyggd\" saknar säkerhetskopiering och kan inte användas med miljo = \"produktion\". Välj \"extern\" eller \"cnpg\"."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Läge: cnpg ---------------------------------------------------------
|
||||
#
|
||||
# Kräver att CloudNativePG-operatorn redan är installerad i klustret —
|
||||
# Terraform slår upp dess CRD vid plan. Installera först:
|
||||
# kubectl apply --server-side -f \
|
||||
# https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.25/releases/cnpg-1.25.0.yaml
|
||||
|
||||
resource "kubernetes_secret_v1" "backup" {
|
||||
count = local.cnpg_databas ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "felsokning-backup"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
labels = local.etiketter
|
||||
}
|
||||
|
||||
type = "Opaque"
|
||||
|
||||
data = {
|
||||
ACCESS_KEY_ID = var.backup_nyckel_id
|
||||
ACCESS_SECRET_KEY = var.backup_nyckel
|
||||
}
|
||||
}
|
||||
|
||||
# Rollen och schemat skapas av operatorn vid initiering. Samma
|
||||
# schemafil som integrationstestet kör, så de kan inte glida isär.
|
||||
resource "kubernetes_secret_v1" "databas_konto" {
|
||||
count = local.cnpg_databas ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "felsokning-db-konto"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
labels = local.etiketter
|
||||
}
|
||||
|
||||
type = "kubernetes.io/basic-auth"
|
||||
|
||||
data = {
|
||||
username = "plattform"
|
||||
password = local.hemligheter["postgres-losenord"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_manifest" "databas" {
|
||||
count = local.cnpg_databas ? 1 : 0
|
||||
|
||||
manifest = {
|
||||
apiVersion = "postgresql.cnpg.io/v1"
|
||||
kind = "Cluster"
|
||||
|
||||
metadata = {
|
||||
name = "felsokning-db"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
labels = local.etiketter
|
||||
}
|
||||
|
||||
spec = {
|
||||
instances = var.databas_instanser
|
||||
imageName = "ghcr.io/cloudnative-pg/postgresql:17.2"
|
||||
|
||||
storage = merge(
|
||||
{ size = var.databas_storlek },
|
||||
var.lagringsklass != "" ? { storageClass = var.lagringsklass } : {},
|
||||
)
|
||||
|
||||
bootstrap = {
|
||||
initdb = {
|
||||
database = "felsokning"
|
||||
owner = "plattform"
|
||||
secret = { name = kubernetes_secret_v1.databas_konto[0].metadata[0].name }
|
||||
|
||||
# Schemat körs som HEL fil, inte som uppdelade satser: det
|
||||
# innehåller en plpgsql-funktion vars kropp har egna semikolon
|
||||
# ($$ ... end $$), och en naiv uppdelning skulle klippa itu den.
|
||||
postInitApplicationSQLRefs = {
|
||||
configMapRefs = [{
|
||||
name = kubernetes_config_map_v1.postgres_init[0].metadata[0].name
|
||||
key = "init.sql"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Basbackup varje natt plus kontinuerlig WAL-arkivering. Det är
|
||||
# WAL-arkivet som ger PITR — utan det är en nattlig kopia bara en
|
||||
# nattlig kopia.
|
||||
backup = {
|
||||
barmanObjectStore = merge(
|
||||
var.backup.endpoint != "" ? { endpointURL = var.backup.endpoint } : {},
|
||||
{
|
||||
destinationPath = var.backup.mal
|
||||
|
||||
s3Credentials = {
|
||||
accessKeyId = {
|
||||
name = kubernetes_secret_v1.backup[0].metadata[0].name
|
||||
key = "ACCESS_KEY_ID"
|
||||
}
|
||||
secretAccessKey = {
|
||||
name = kubernetes_secret_v1.backup[0].metadata[0].name
|
||||
key = "ACCESS_SECRET_KEY"
|
||||
}
|
||||
}
|
||||
|
||||
wal = { compression = "gzip" }
|
||||
data = { compression = "gzip" }
|
||||
},
|
||||
)
|
||||
|
||||
retentionPolicy = "${var.backup.behall_dagar}d"
|
||||
}
|
||||
|
||||
resources = {
|
||||
requests = { cpu = "250m", memory = "512Mi" }
|
||||
limits = { cpu = "2", memory = "2Gi" }
|
||||
}
|
||||
|
||||
# Sprid instanserna så att en nod som försvinner inte tar hela
|
||||
# klustret med sig.
|
||||
affinity = { enablePodAntiAffinity = true, topologyKey = "kubernetes.io/hostname" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_manifest" "backup_schema" {
|
||||
count = local.cnpg_databas ? 1 : 0
|
||||
|
||||
manifest = {
|
||||
apiVersion = "postgresql.cnpg.io/v1"
|
||||
kind = "ScheduledBackup"
|
||||
|
||||
metadata = {
|
||||
name = "felsokning-db-natt"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
labels = local.etiketter
|
||||
}
|
||||
|
||||
spec = {
|
||||
# Sekund-fältet först (CNPG använder sex fält): 02:30 varje natt.
|
||||
schedule = "0 30 2 * * *"
|
||||
backupOwnerReference = "self"
|
||||
cluster = { name = "felsokning-db" }
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [kubernetes_manifest.databas]
|
||||
}
|
||||
|
||||
# ---- Läge: inbyggd ------------------------------------------------------
|
||||
#
|
||||
# En StatefulSet med en volym. Ingen säkerhetskopiering, ingen failover.
|
||||
# Precondition ovan stoppar läget i produktionsmiljö.
|
||||
|
||||
resource "kubernetes_stateful_set_v1" "postgres" {
|
||||
count = local.inbyggd_databas ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "postgres"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
@@ -123,7 +326,7 @@ resource "kubernetes_stateful_set_v1" "postgres" {
|
||||
name = "init"
|
||||
|
||||
config_map {
|
||||
name = kubernetes_config_map_v1.postgres_init.metadata[0].name
|
||||
name = kubernetes_config_map_v1.postgres_init[0].metadata[0].name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,6 +350,8 @@ resource "kubernetes_stateful_set_v1" "postgres" {
|
||||
}
|
||||
|
||||
resource "kubernetes_service_v1" "postgres" {
|
||||
count = local.inbyggd_databas ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "postgres"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
|
||||
@@ -72,20 +72,26 @@ resource "kubernetes_deployment_v1" "plattform" {
|
||||
}
|
||||
}
|
||||
|
||||
env {
|
||||
name = "POSTGRES_LOSENORD"
|
||||
# Lösenordet expanderas in i DATABASE_URL nedan. I externt läge
|
||||
# står hela anslutningen i variabeln och behövs inte här.
|
||||
dynamic "env" {
|
||||
for_each = local.extern_databas ? [] : [1]
|
||||
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
||||
key = "postgres-losenord"
|
||||
content {
|
||||
name = "POSTGRES_LOSENORD"
|
||||
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
||||
key = "postgres-losenord"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env {
|
||||
name = "DATABASE_URL"
|
||||
value = "postgresql://plattform:$(POSTGRES_LOSENORD)@postgres:5432/felsokning"
|
||||
value = local.databas_anslutning
|
||||
}
|
||||
|
||||
# Klienten serveras från samma domän som API:t, så CORS behöver
|
||||
|
||||
@@ -22,8 +22,12 @@ resource "kubernetes_network_policy_v1" "neka_allt_in" {
|
||||
}
|
||||
}
|
||||
|
||||
# Databasen: bara plattformstjänsten, bara 5432.
|
||||
# Databasen: bara plattformstjänsten, bara 5432. Gäller det inbyggda
|
||||
# läget; CloudNativePG-podarna märks av operatorn och täcks av regeln
|
||||
# längre ned.
|
||||
resource "kubernetes_network_policy_v1" "postgres_in" {
|
||||
count = local.inbyggd_databas ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "postgres-endast-fran-plattform"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
@@ -132,6 +136,8 @@ resource "kubernetes_network_policy_v1" "web_ut" {
|
||||
|
||||
# Databasen ringer heller ingenting.
|
||||
resource "kubernetes_network_policy_v1" "postgres_ut" {
|
||||
count = local.inbyggd_databas ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "postgres-inget-utgaende"
|
||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||
@@ -165,16 +171,23 @@ resource "kubernetes_network_policy_v1" "plattform_ut" {
|
||||
|
||||
policy_types = ["Egress"]
|
||||
|
||||
egress {
|
||||
to {
|
||||
pod_selector {
|
||||
match_labels = { app = "postgres" }
|
||||
}
|
||||
}
|
||||
# Databasen i klustret — inbyggt eller CloudNativePG. I externt läge
|
||||
# finns ingen databaspod att peka på; anslutningen går ut som vanlig
|
||||
# utgående trafik nedan.
|
||||
dynamic "egress" {
|
||||
for_each = local.extern_databas ? [] : [1]
|
||||
|
||||
ports {
|
||||
port = "5432"
|
||||
protocol = "TCP"
|
||||
content {
|
||||
to {
|
||||
pod_selector {
|
||||
match_labels = local.inbyggd_databas ? { app = "postgres" } : { "cnpg.io/cluster" = "felsokning-db" }
|
||||
}
|
||||
}
|
||||
|
||||
ports {
|
||||
port = "5432"
|
||||
protocol = "TCP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +212,18 @@ resource "kubernetes_network_policy_v1" "plattform_ut" {
|
||||
protocol = "TCP"
|
||||
}
|
||||
}
|
||||
|
||||
# Managerad databas utanför klustret: 5432 mot leverantören.
|
||||
dynamic "egress" {
|
||||
for_each = local.extern_databas ? [1] : []
|
||||
|
||||
content {
|
||||
ports {
|
||||
port = "5432"
|
||||
protocol = "TCP"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-25
@@ -9,7 +9,7 @@ begripliga att läsa:
|
||||
| `variables.tf` | Allt som skiljer en installation från en annan |
|
||||
| `karta.tf` | **Systemet som data** — tjänster, portar, routing, hemligheter, dataflöden, gränser |
|
||||
| `10-namnrymd.tf` | Namnrymd, hemligheten, databasschemat |
|
||||
| `20-databas.tf` | Postgres — händelseloggen |
|
||||
| `20-databas.tf` | Händelseloggen — tre lägen, se nedan |
|
||||
| `30-plattform.tf` | Backend: auth, händelse-API, delning, integrationer |
|
||||
| `40-orkester.tf` | AI-orkestern |
|
||||
| `50-web.tf` | Klienten |
|
||||
@@ -45,41 +45,51 @@ terraform validate # typer och referenser
|
||||
`terraform validate` kräver att leverantörerna hämtats från
|
||||
registry.terraform.io.
|
||||
|
||||
## Förhållandet till kustomize och Argo CD
|
||||
## Databasen: tre lägen
|
||||
|
||||
`infra/k8s` + `infra/overlays` + `infra/gitops` beskriver **samma system**
|
||||
i kustomize, synkat av Argo CD. Det är en historisk parallell väg.
|
||||
`var.databas_lage` saknar standardvärde med flit. Valet avgör om det
|
||||
finns säkerhetskopiering, och det ska inte kunna bli fel av slentrian.
|
||||
|
||||
**Välj en.** Kör båda mot samma kluster och de motarbetar varandra: Argo
|
||||
CD:s `selfHeal` återställer det Terraform just ändrat, och `prune` tar
|
||||
bort det Terraform skapat.
|
||||
| Läge | Backup | Failover | Använd när |
|
||||
| --- | --- | --- | --- |
|
||||
| `extern` | Leverantörens, med PITR | Leverantörens | **Produktion.** Cloud SQL, RDS, Neon, Azure Flexible Server |
|
||||
| `cnpg` | Basbackup 02:30 + WAL-arkiv → objektlagring, PITR | Ja, `databas_instanser` styr | Produktion när databasen måste ligga i klustret |
|
||||
| `inbyggd` | **Ingen** | Nej | Prov och demo. Blockeras av en precondition när `miljo = "produktion"` |
|
||||
|
||||
Rekommendationen är Terraform, av tre skäl:
|
||||
Går händelseloggen förlorad är det inte "data" som försvinner utan varje
|
||||
ärendes bevisvärde: vad som kontrollerades, av vem, när, med vilken
|
||||
evidens. Det går inte att återskapa i efterhand.
|
||||
|
||||
1. Nätverkspolicyer, genererade hemligheter och (senare) molnresurser
|
||||
som databas, DNS och objektlagring hör hemma här.
|
||||
2. Kartan blir läsbar — `karta.tf` och `terraform output` finns inte i
|
||||
kustomize-varianten.
|
||||
3. Kustomize-vägen saknar i dag nätverkspolicyer och kör Postgres utan
|
||||
säkerhetskontext.
|
||||
`cnpg` kräver att CloudNativePG-operatorn redan är installerad —
|
||||
Terraform slår upp dess CRD vid plan:
|
||||
|
||||
Byter ni: ta bort Argo CD-applikationen (`kubectl -n argocd delete
|
||||
application guidad-felsokning`) **innan** första `terraform apply`, och
|
||||
importera befintliga resurser med `terraform import` om ni vill undvika
|
||||
omstart. Behåller ni Argo CD i stället: applicera nätverkspolicyerna och
|
||||
ingressens kroppsgräns därifrån också.
|
||||
```sh
|
||||
kubectl apply --server-side -f \
|
||||
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.25/releases/cnpg-1.25.0.yaml
|
||||
```
|
||||
|
||||
Bildtaggen kommer från publiceringsflödet oavsett väg — kör
|
||||
`terraform apply -var bildtagg=<git-sha>` i stället för att låta CI
|
||||
skriva i git.
|
||||
I `extern` läge ansvarar ni själva för att köra
|
||||
`infra/postgres-init.sql` mot databasen. Det är samma fil som
|
||||
integrationstestet kör, så schemat kan inte glida isär från det som
|
||||
testas.
|
||||
|
||||
## Driftsättning
|
||||
|
||||
Bilderna byggs av `publicera.yml` vid varje push till main och taggas med
|
||||
git-SHA:t. Driftsättningen är ett eget flöde, `driftsatt.yml`, som
|
||||
startas för hand med en tagg och kör mot GitHub-miljön `produktion` — den
|
||||
kan kräva godkännande. Rollback är att köra flödet igen med en tidigare
|
||||
tagg.
|
||||
|
||||
Kustomize- och Argo CD-vägen är borttagen. Den beskrev samma system en
|
||||
gång till och kunde inte köras samtidigt som Terraform utan att de
|
||||
motarbetade varandra (`selfHeal` återställde det Terraform ändrade,
|
||||
`prune` tog bort det Terraform skapade).
|
||||
|
||||
## Det som medvetet inte ingår
|
||||
|
||||
Står också i `terraform output karta` under `avgränsningar`:
|
||||
|
||||
- **Säkerhetskopiering.** En StatefulSet med en PVC är inte backup. Sätt
|
||||
CloudNativePG (basbackup, WAL-arkivering, PITR, failover) innan skarp
|
||||
drift och peka `DATABASE_URL` på dess tjänst.
|
||||
- **Objektlagring.** Foton och video ligger som data-URL:er i
|
||||
händelseloggen. Det gör loggen till systemets enda sanningskälla, men
|
||||
också stor och tung att säkerhetskopiera.
|
||||
|
||||
+17
-12
@@ -46,10 +46,15 @@ locals {
|
||||
utat = ["api.anthropic.com"]
|
||||
}
|
||||
postgres = {
|
||||
roll = "Händelseloggen — systemets enda sanningskälla. Append-only garanteras av databastriggers, inte bara av API:t."
|
||||
bild = "postgres:17-alpine"
|
||||
roll = join(" ", [
|
||||
"Händelseloggen — systemets enda sanningskälla. Append-only garanteras av databastriggers, inte bara av API:t.",
|
||||
var.databas_lage == "extern" ? "Läge: extern managerad Postgres — leverantören sköter backup och PITR." :
|
||||
var.databas_lage == "cnpg" ? "Läge: CloudNativePG i klustret — basbackup, WAL-arkivering, PITR och failover." :
|
||||
"Läge: inbyggd StatefulSet UTAN säkerhetskopiering — endast prov och demo."
|
||||
])
|
||||
bild = var.databas_lage == "cnpg" ? "ghcr.io/cloudnative-pg/postgresql:17.2" : var.databas_lage == "extern" ? "(utanför klustret)" : "postgres:17-alpine"
|
||||
hemligt = ["postgres-losenord"]
|
||||
utat = []
|
||||
utat = var.databas_lage == "cnpg" ? ["objektlagringen för backup och WAL-arkiv"] : []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +84,6 @@ locals {
|
||||
)
|
||||
}
|
||||
|
||||
databas_url = "postgresql://plattform:${local.hemligheter["postgres-losenord"]}@postgres:5432/felsokning"
|
||||
|
||||
# ---- Dataflöden -----------------------------------------------------
|
||||
#
|
||||
# Det som är värt att förstå innan man ändrar något: vad som rör sig
|
||||
@@ -102,13 +105,15 @@ locals {
|
||||
|
||||
# ---- Det som medvetet inte ingår ------------------------------------
|
||||
|
||||
avgransningar = [
|
||||
"Säkerhetskopiering av databasen. StatefulSet + PVC är inte backup — sätt CloudNativePG eller motsvarande innan skarp drift.",
|
||||
"Objektlagring. Foton och video ligger som data-URL:er i händelseloggen, vilket gör volymen stor och tung att säkerhetskopiera.",
|
||||
"Observability. Ingen metrikexport, ingen tracing — bara containerloggar.",
|
||||
"Takt-begränsning på inloggning. Endast den publika beslutsvägen är begränsad, och bara per pod.",
|
||||
"Återkallelse av utfärdade JWT. En token gäller sin livstid ut även om användaren tas bort.",
|
||||
]
|
||||
avgransningar = concat(
|
||||
var.databas_lage == "inbyggd" ? ["INGEN SÄKERHETSKOPIERING — läget inbyggd har en volym och inget mer. Endast prov och demo."] : [],
|
||||
[
|
||||
"Objektlagring. Foton och video ligger som data-URL:er i händelseloggen, vilket gör volymen stor och tung att säkerhetskopiera.",
|
||||
"Observability. Ingen metrikexport, ingen tracing — bara containerloggar.",
|
||||
"Takt-begränsning på inloggning. Endast den publika beslutsvägen är begränsad, och bara per pod.",
|
||||
"Återkallelse av utfärdade JWT. En token gäller sin livstid ut även om användaren tas bort.",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
# Genereras bara när motsvarande variabel lämnats tom.
|
||||
|
||||
@@ -29,10 +29,24 @@ output "karta" {
|
||||
gränser = local.granser
|
||||
|
||||
databas = {
|
||||
läge = var.databas_lage
|
||||
motor = "PostgreSQL 17"
|
||||
volym = var.databas_storlek
|
||||
säkerhet = "append-only via triggers — historik kan inte ändras eller raderas av någon roll"
|
||||
backup = "INGEN i den här definitionen — se avgränsningar"
|
||||
|
||||
backup = (
|
||||
var.databas_lage == "extern"
|
||||
? "Leverantörens — verifiera att PITR faktiskt är påslaget."
|
||||
: var.databas_lage == "cnpg"
|
||||
? "Basbackup 02:30 + kontinuerlig WAL-arkivering till ${var.backup.mal}, ${var.backup.behall_dagar} dagars retention. PITR möjlig."
|
||||
: "INGEN. Går volymen förlorad är händelseloggen borta."
|
||||
)
|
||||
|
||||
volym = var.databas_lage == "extern" ? "(leverantörens)" : var.databas_storlek
|
||||
instanser = (
|
||||
var.databas_lage == "cnpg"
|
||||
? "${var.databas_instanser} (failover)"
|
||||
: var.databas_lage == "extern" ? "(leverantörens)" : "1 (ingen failover)"
|
||||
)
|
||||
}
|
||||
|
||||
drift = {
|
||||
|
||||
@@ -27,6 +27,30 @@ registrering_oppen = false
|
||||
# adresser. Sätt bara true om verkstaden har en OEM-server på eget nät.
|
||||
tillat_interna_uppslag = false
|
||||
|
||||
# --- Databasen ---------------------------------------------------------
|
||||
#
|
||||
# Inget standardvärde: valet avgör om det finns säkerhetskopiering.
|
||||
#
|
||||
# "extern" managerad Postgres — leverantören sköter backup och PITR
|
||||
# (rekommenderat i produktion). Kräver databas_url.
|
||||
# "cnpg" CloudNativePG i klustret — basbackup, WAL-arkiv, failover.
|
||||
# Kräver att operatorn är installerad och backup-uppgifter.
|
||||
# "inbyggd" en volym, ingen backup. Endast prov och demo — går inte
|
||||
# att kombinera med miljo = "produktion".
|
||||
|
||||
databas_lage = "extern"
|
||||
databas_url = "postgresql://plattform:...@db.exempel.internal:5432/felsokning"
|
||||
|
||||
# Används bara när databas_lage = "cnpg":
|
||||
# backup = {
|
||||
# mal = "s3://verkstad-backup/felsokning"
|
||||
# endpoint = "" # sätts för S3-kompatibel lagring som inte är AWS
|
||||
# behall_dagar = 30
|
||||
# }
|
||||
# backup_nyckel_id = ""
|
||||
# backup_nyckel = ""
|
||||
# databas_instanser = 3
|
||||
|
||||
databas_storlek = "50Gi"
|
||||
# lagringsklass = "premium-rwo"
|
||||
|
||||
|
||||
@@ -169,6 +169,79 @@ variable "max_repliker" {
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Databas -----------------------------------------------------------
|
||||
|
||||
variable "databas_lage" {
|
||||
description = <<-TEXT
|
||||
Hur händelseloggen lagras. Inget standardvärde: valet avgör om det
|
||||
finns säkerhetskopiering, och det är inte ett val någon ska göra av
|
||||
misstag.
|
||||
|
||||
"extern" Managerad Postgres utanför klustret (Cloud SQL, RDS,
|
||||
Neon, Azure Flexible Server). Leverantören sköter
|
||||
backup, PITR, failover och kryptering. REKOMMENDERAT
|
||||
i produktion. Kräver databas_url.
|
||||
|
||||
"cnpg" CloudNativePG i klustret: basbackup, WAL-arkivering och
|
||||
PITR mot objektlagring. Kräver att operatorn redan är
|
||||
installerad — Terraform slår upp dess CRD vid plan.
|
||||
Kräver backup-uppgifterna nedan.
|
||||
|
||||
"inbyggd" En StatefulSet med en volym. INGEN säkerhetskopiering.
|
||||
Går volymen förlorad är händelseloggen borta, och den
|
||||
är hela produktens bevisvärde. Endast för prov och demo.
|
||||
TEXT
|
||||
type = string
|
||||
|
||||
validation {
|
||||
condition = contains(["extern", "cnpg", "inbyggd"], var.databas_lage)
|
||||
error_message = "databas_lage måste vara \"extern\", \"cnpg\" eller \"inbyggd\"."
|
||||
}
|
||||
}
|
||||
|
||||
variable "databas_url" {
|
||||
description = "Anslutning till den externa databasen. Krävs när databas_lage = \"extern\"."
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "backup" {
|
||||
description = <<-TEXT
|
||||
Objektlagring för CloudNativePG:s basbackup och WAL-arkiv. Används
|
||||
bara när databas_lage = "cnpg". mal är en S3-URL, t.ex.
|
||||
s3://verkstad-backup/felsokning.
|
||||
TEXT
|
||||
type = object({
|
||||
mal = string
|
||||
endpoint = optional(string, "")
|
||||
behall_dagar = optional(number, 30)
|
||||
})
|
||||
default = {
|
||||
mal = ""
|
||||
}
|
||||
}
|
||||
|
||||
variable "backup_nyckel_id" {
|
||||
description = "Åtkomstnyckel till objektlagringen (databas_lage = \"cnpg\")."
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "backup_nyckel" {
|
||||
description = "Hemlig nyckel till objektlagringen (databas_lage = \"cnpg\")."
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "databas_instanser" {
|
||||
description = "Antal Postgres-instanser i CloudNativePG-klustret. 3 ger failover utan dataförlust."
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
variable "databas_storlek" {
|
||||
description = "Volymstorlek för händelseloggen. Foton och video ligger inline i loggen."
|
||||
type = string
|
||||
|
||||
Reference in New Issue
Block a user