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:
@@ -33,6 +33,23 @@ jobs:
|
||||
- run: sudo bash integrationstest.sh
|
||||
working-directory: services/plattform
|
||||
|
||||
# Kontrollerar infrastrukturens definition utan kluster: formatering,
|
||||
# leverantörer, typer och referenser — och att alla tre databaslägena
|
||||
# går att planera.
|
||||
terraform:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: infra/terraform
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: 1.9.8
|
||||
- run: terraform fmt -check -recursive
|
||||
- run: terraform init -backend=false -input=false
|
||||
- run: terraform validate
|
||||
|
||||
container-byggen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Driftsättning: terraform apply mot klustret.
|
||||
#
|
||||
# Startas för hand med en bildtagg från publiceringsflödet. Det är
|
||||
# avsiktligt — driftsättning är ett beslut, och miljön `produktion` kan
|
||||
# kräva godkännande i GitHub innan jobbet får köra.
|
||||
#
|
||||
# Rollback = kör flödet igen med en tidigare tagg.
|
||||
#
|
||||
# Krävs på miljön `produktion`:
|
||||
# secrets KUBE_CONFIG (base64), TF_API_TOKEN eller backend-uppgifter,
|
||||
# ANTHROPIC_API_NYCKEL, DATABAS_URL
|
||||
# vars DOMAN, REGISTER, DATABAS_LAGE
|
||||
name: Driftsätt
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bildtagg:
|
||||
description: "Bildtagg att driftsätta (git-SHA från Publicera)"
|
||||
required: true
|
||||
type: string
|
||||
bara_plan:
|
||||
description: "Visa bara planen, applicera inte"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
driftsatt:
|
||||
runs-on: ubuntu-latest
|
||||
environment: produktion
|
||||
defaults:
|
||||
run:
|
||||
working-directory: infra/terraform
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: 1.9.8
|
||||
|
||||
- name: Kubeconfig
|
||||
run: |
|
||||
mkdir -p "$HOME/.kube"
|
||||
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$HOME/.kube/config"
|
||||
chmod 600 "$HOME/.kube/config"
|
||||
|
||||
- name: Formatering
|
||||
run: terraform fmt -check -recursive
|
||||
|
||||
- name: Init
|
||||
run: terraform init -input=false
|
||||
|
||||
- name: Validering
|
||||
run: terraform validate
|
||||
|
||||
- name: Plan
|
||||
run: terraform plan -input=false -out=plan.tfplan
|
||||
env:
|
||||
TF_VAR_bildtagg: ${{ inputs.bildtagg }}
|
||||
TF_VAR_doman: ${{ vars.DOMAN }}
|
||||
TF_VAR_register: ${{ vars.REGISTER }}
|
||||
TF_VAR_databas_lage: ${{ vars.DATABAS_LAGE }}
|
||||
TF_VAR_databas_url: ${{ secrets.DATABAS_URL }}
|
||||
TF_VAR_anthropic_api_nyckel: ${{ secrets.ANTHROPIC_API_NYCKEL }}
|
||||
TF_VAR_jwt_hemlighet: ${{ secrets.JWT_HEMLIGHET }}
|
||||
TF_VAR_postgres_losenord: ${{ secrets.POSTGRES_LOSENORD }}
|
||||
TF_VAR_integration_nyckel: ${{ secrets.INTEGRATION_NYCKEL }}
|
||||
|
||||
- name: Applicera
|
||||
if: ${{ !inputs.bara_plan }}
|
||||
run: terraform apply -input=false plan.tfplan
|
||||
|
||||
- name: Kartan efter driftsättning
|
||||
if: ${{ !inputs.bara_plan }}
|
||||
run: |
|
||||
{
|
||||
echo "### Driftsatt \`${{ inputs.bildtagg }}\`"
|
||||
echo ""
|
||||
echo '```'
|
||||
terraform output -no-color karta
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Rök: klienten, API-specen och hälsokontrollen ska svara.
|
||||
- name: Rökkontroll
|
||||
if: ${{ !inputs.bara_plan }}
|
||||
run: |
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if curl -fsS "https://${{ vars.DOMAN }}/halsa" > /dev/null; then
|
||||
echo "hälsokontrollen svarar"
|
||||
curl -fsS "https://${{ vars.DOMAN }}/api/openapi.yaml" | head -1
|
||||
exit 0
|
||||
fi
|
||||
echo "väntar på att tjänsten ska svara ($i/10)"
|
||||
sleep 15
|
||||
done
|
||||
echo "tjänsten svarade inte inom 150 s"
|
||||
exit 1
|
||||
@@ -1,17 +1,15 @@
|
||||
# CD-halvan av GitOps-flödet: varje push till main bygger och publicerar
|
||||
# de tre bilderna till GHCR taggade med git-SHA:t, och uppdaterar sedan
|
||||
# produktions-overlayens taggar i git. Argo CD ser ändringen och synkar
|
||||
# klustret — ingen kubectl-åtkomst från CI, klustret drar själv.
|
||||
# Bygger och publicerar de tre bilderna till GHCR, taggade med git-SHA:t.
|
||||
# Driftsättningen sker i ett eget flöde (driftsatt.yml) som kör
|
||||
# terraform apply mot en miljö med godkännandekrav — en bild som ligger i
|
||||
# registret är alltså inte samma sak som en bild som kör.
|
||||
name: Publicera
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "infra/overlays/**" # gitops-commiten ska inte trigga en ny publicering
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
@@ -28,34 +26,34 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Klientens API-adress bakas in vid bygget (Vite), så bilden är
|
||||
# miljöbunden — en bild byggd mot en domän pekar på fel API i en
|
||||
# annan miljö.
|
||||
- name: Bygg och publicera web
|
||||
run: |
|
||||
docker build -t "$REGISTER/guidad-felsokning-web:${{ github.sha }}" \
|
||||
--build-arg VITE_PLATTFORM_URL="${{ vars.PLATTFORM_URL }}" \
|
||||
--build-arg VITE_AI_ORKESTER_URL="${{ vars.PLATTFORM_URL }}" .
|
||||
docker push "$REGISTER/guidad-felsokning-web:${{ github.sha }}"
|
||||
|
||||
- name: Bygg och publicera ai-orkester
|
||||
run: |
|
||||
docker build -t "$REGISTER/guidad-felsokning-ai-orkester:${{ github.sha }}" services/ai-orkester
|
||||
docker push "$REGISTER/guidad-felsokning-ai-orkester:${{ github.sha }}"
|
||||
|
||||
- name: Bygg och publicera plattform
|
||||
run: |
|
||||
docker build -t "$REGISTER/guidad-felsokning-plattform:${{ github.sha }}" services/plattform
|
||||
docker push "$REGISTER/guidad-felsokning-plattform:${{ github.sha }}"
|
||||
|
||||
- name: Uppdatera produktions-overlayen (GitOps)
|
||||
- name: Sammanfatta
|
||||
run: |
|
||||
curl -sL "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.3/kustomize_v5.4.3_linux_amd64.tar.gz" | tar xz
|
||||
cd infra/overlays/produktion
|
||||
for tjanst in web ai-orkester plattform; do
|
||||
../../../kustomize edit set image \
|
||||
"ghcr.io/ORGANISATION/guidad-felsokning-$tjanst=$REGISTER/guidad-felsokning-$tjanst:${{ github.sha }}"
|
||||
done
|
||||
../../../kustomize build . > /dev/null # verifiera att overlayen fortfarande bygger
|
||||
- name: Committa overlay-uppdateringen
|
||||
run: |
|
||||
git config user.name "gitops-bot"
|
||||
git config user.email "gitops@users.noreply.github.com"
|
||||
git add infra/overlays/produktion/kustomization.yaml
|
||||
git diff --cached --quiet || git commit -m "gitops: driftsätt ${{ github.sha }} [skip ci]"
|
||||
git push
|
||||
{
|
||||
echo "### Publicerat"
|
||||
echo ""
|
||||
echo '```'
|
||||
echo "bildtagg: ${{ github.sha }}"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "Driftsätt med flödet **Driftsätt** och den taggen."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
+54
-60
@@ -21,7 +21,7 @@ flowchart LR
|
||||
| `web` | SPA:n bakom oprivilegierad nginx (`Dockerfile`, `docker/nginx.conf`) | Deployment + Service + HPA + PDB |
|
||||
| `plattform` | Självhostad backend (`services/plattform`): **multi-tenant** — registrering skapar organisation + systemadministratör, admin hanterar användare (tekniker/arbetsledare/admin), all ärendedata organisationsisolerad. Inloggning (bcrypt via pgcrypto, HS256-JWT med roll + org i anspråken), append-only händelse-API, publik delningsendpoint | Deployment + Service + HPA + PDB |
|
||||
| `ai-orkester` | AI-orkestern (`services/ai-orkester`): fyra uppgifter routade till Sonnet 5 / Opus 5 / Haiku 4.5 — verifierar plattformens JWT (delad hemlighet) | Deployment + Service + HPA + PDB |
|
||||
| `postgres` | Händelselogg + användare; **append-only garanterat med databastriggers** — historik kan inte ändras eller raderas oavsett roll | StatefulSet + PVC (10 Gi). Produktion: CloudNativePG-operatorn för backup/failover/PITR |
|
||||
| `postgres` | Händelselogg + användare; **append-only garanterat med databastriggers** — historik kan inte ändras eller raderas oavsett roll | Tre lägen: extern managerad Postgres (rekommenderat), CloudNativePG i klustret, eller en enkel StatefulSet utan backup för prov |
|
||||
| Hemligheter | `anthropic-api-key`, `jwt-secret` (delas av plattform + orkester), `postgres-losenord`, `integration-nyckel` (krypterar kundernas märkesspecifika credentials) |
|
||||
| Miljöflaggor | `TILLATNA_URSPRUNG` (CORS-lista; utelämnad = `*`), `TILLAT_INTERNA_UPPSLAG` (`true` tillåter leverantörsuppslag mot privata nät), `REGISTRERING_OPPEN`, `ECM_REGLER_FIL`, `INTEGRATIONER_FIL` | Secret `felsokning-hemligheter` — aldrig i bilder eller manifest |
|
||||
|
||||
@@ -34,11 +34,11 @@ Börja i `karta.tf`: hela systemet beskrivet en gång som data (tjänster,
|
||||
portar, routing, hemligheter, dataflöden, gränser). `terraform output
|
||||
karta` skriver ut samma sak i klartext.
|
||||
|
||||
`infra/k8s` + `infra/overlays` + `infra/gitops` beskriver samma system i
|
||||
kustomize, synkat av Argo CD. **Kör inte båda mot samma kluster** — Argo
|
||||
CD:s `selfHeal` återställer det Terraform ändrar och `prune` tar bort det
|
||||
Terraform skapar. Terraform-vägen har dessutom nätverkspolicyer och
|
||||
säkerhetskontext på databasen, vilket kustomize-vägen saknar.
|
||||
Definitionen omfattar hemligheter, databasschema, nätverksgränser och
|
||||
alla tre databaslägena. Kustomize- och Argo CD-vägen är borttagen —
|
||||
`infra/postgres-init.sql` är det enda som blivit kvar utanför Terraform,
|
||||
och den läses av både Terraform och integrationstestet så att schemat
|
||||
inte kan glida isär från det som testas.
|
||||
|
||||
## Nätverksgränser
|
||||
|
||||
@@ -61,59 +61,52 @@ dokumentation, inte skydd.
|
||||
|
||||
## Driftsätta
|
||||
|
||||
Allt går genom Terraform — hemligheter, schema och nätverksgränser
|
||||
ingår. Det finns ingen `kubectl apply` att komma ihåg.
|
||||
|
||||
```sh
|
||||
# 1. Bygg och publicera bilderna (ersätt registry i infra/k8s/*.yaml)
|
||||
docker build -t ghcr.io/ORG/guidad-felsokning-web \
|
||||
--build-arg VITE_PLATTFORM_URL=https://app.exempel.se .
|
||||
docker build -t ghcr.io/ORG/guidad-felsokning-ai-orkester services/ai-orkester
|
||||
docker build -t ghcr.io/ORG/guidad-felsokning-plattform services/plattform
|
||||
docker push ghcr.io/ORG/guidad-felsokning-web
|
||||
docker push ghcr.io/ORG/guidad-felsokning-ai-orkester
|
||||
docker push ghcr.io/ORG/guidad-felsokning-plattform
|
||||
cd infra/terraform
|
||||
cp terraform.tfvars.exempel terraform.tfvars # domän, register, databasläge, nycklar
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply -var bildtagg=<git-sha>
|
||||
|
||||
# 2. Skapa secret:en (eller använd External Secrets/Sealed Secrets)
|
||||
kubectl create namespace guidad-felsokning
|
||||
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)"
|
||||
|
||||
# 3. Applicera manifesten (Postgres initieras med schema + append-only-triggers)
|
||||
kubectl apply -k infra/k8s
|
||||
|
||||
# 4. Verifiera
|
||||
kubectl -n guidad-felsokning get pods
|
||||
curl https://app.exempel.se/halsa # → {"status":"ok"} (plattformen)
|
||||
curl https://app.exempel.se/api/openapi.yaml # API-first: hela API-specen
|
||||
terraform output karta # hela systemet i klartext
|
||||
terraform output endpoints # adresser att kontrollera
|
||||
```
|
||||
|
||||
Med Terraform i stället: `cd infra/terraform && terraform apply -var bildtagg=<git-sha>`.
|
||||
Sedan:
|
||||
|
||||
Byt domän och cert-issuer i `infra/k8s/ingress.yaml` (eller `var.doman` i Terraform). Att skapa nya organisationer är öppet i beta — stäng med `REGISTRERING_OPPEN=false` på plattformens Deployment; användare inom en organisation skapas alltid av dess systemadministratör.
|
||||
```sh
|
||||
curl https://app.exempel.se/halsa # → {"status":"ok"}
|
||||
curl https://app.exempel.se/api/openapi.yaml # hela API-specen
|
||||
```
|
||||
|
||||
## Märkesspecifika kopplingar
|
||||
Klustret behöver: en CNI som tillämpar NetworkPolicy, ingress-nginx,
|
||||
cert-manager, en metrics-server och en StorageClass med ReadWriteOnce.
|
||||
|
||||
Varje verkstad har sina egna avtal med tillverkare och dataleverantörer.
|
||||
Kopplingarna konfigureras därför av kunden själv under **Inställningar →
|
||||
Märkesspecifika kopplingar**: systemadministratören väljer leverantör och
|
||||
fyller i sina credentials.
|
||||
Att skapa nya organisationer är stängt som standard
|
||||
(`registrering_oppen = false`); användare inom en organisation skapas
|
||||
alltid av dess systemadministratör.
|
||||
|
||||
* **Uppgifterna når aldrig webbläsaren.** De krypteras med AES-256-GCM
|
||||
(`INTEGRATION_NYCKEL`, 32 byte hex eller base64) innan de skrivs till
|
||||
tabellen `integrationer`, och API:t returnerar hemliga fält maskerade
|
||||
(`••••3456`). Alla uppslag mot leverantören görs av servern.
|
||||
* **Fail closed.** Saknas `INTEGRATION_NYCKEL` sparas ingenting — API:t
|
||||
svarar 503 och inställningssidan säger varför. Inga uppgifter hamnar
|
||||
någonsin i klartext.
|
||||
* **Leverantörer är data, inte kod.** Registret ligger i
|
||||
`services/plattform/integrationer.json` och kan bytas mot en
|
||||
ConfigMap-mount via `INTEGRATIONER_FIL`. Nya märken läggs till genom
|
||||
att beskriva URL-mall, autentiseringstyp och svarsmappning — ingen
|
||||
ombyggnad av applikationen krävs.
|
||||
* **Testresultat loggas på kopplingen.** Varje uppslag skriver
|
||||
`senast_testad` och `senaste_status`, så ett trasigt abonnemang syns i
|
||||
inställningarna i stället för att tyst ge tomma svar.
|
||||
## Databasen: valet som avgör om det finns backup
|
||||
|
||||
`databas_lage` saknar standardvärde med flit.
|
||||
|
||||
| Läge | Backup | Failover | Använd när |
|
||||
| --- | --- | --- | --- |
|
||||
| `extern` | Leverantörens, med PITR | Leverantörens | **Produktion.** Cloud SQL, RDS, Neon, Azure |
|
||||
| `cnpg` | Basbackup 02:30 + WAL-arkiv → objektlagring, PITR | Ja | Produktion när databasen måste ligga i klustret |
|
||||
| `inbyggd` | **Ingen** | Nej | Prov och demo — spärras när `miljo = "produktion"` |
|
||||
|
||||
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.
|
||||
|
||||
`cnpg` kräver CloudNativePG-operatorn installerad först — Terraform slår
|
||||
upp dess CRD redan vid plan. I `extern` läge kör ni
|
||||
`infra/postgres-init.sql` mot databasen själva; det är samma fil som
|
||||
integrationstestet kör.
|
||||
|
||||
## Multi-tenant och roller
|
||||
|
||||
@@ -137,18 +130,19 @@ Integrationstestet (`services/plattform/integrationstest.sh`, körs även i CI m
|
||||
|
||||
**CI** (`.github/workflows/ci.yml`): tester, produktionsbygge, integrationstest mot riktig Postgres och verifierande containerbyggen på varje push/PR.
|
||||
|
||||
**CD** (`.github/workflows/publicera.yml` + Argo CD): klustret följer git — ingen CI-process har kubectl-åtkomst.
|
||||
**CD** — två flöden, medvetet åtskilda: en bild i registret är inte samma sak som en bild som kör.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P[Push till main] --> B[Bygg + publicera\n3 bilder till GHCR\ntaggade med git-SHA]
|
||||
B --> O[Uppdatera\ninfra/overlays/produktion\n+ commit till git]
|
||||
O --> A[Argo CD ser ändringen] --> S[Synkar klustret\nprune + selfHeal]
|
||||
P[Push till main] --> B[Publicera:\nbygger 3 bilder\ntaggade med git-SHA] --> G[(GHCR)]
|
||||
G -.-> D[Driftsätt:\nstartas för hand\nmed en tagg]
|
||||
D --> M[miljö: produktion\ngodkännande] --> T[terraform apply] --> K[Klustret]
|
||||
T --> R[Rökkontroll\nhälsa + API-spec]
|
||||
```
|
||||
|
||||
1. Varje main-push bygger de tre bilderna, publicerar till GHCR (`GITHUB_TOKEN`, inga externa hemligheter) och uppdaterar produktions-overlayens taggar med `kustomize edit set image` — overlayen ombyggs som verifiering innan commiten.
|
||||
2. **Argo CD är enda vägen in i klustret.** Bootstrap en gång: installera Argo CD, ersätt repo-URL:en i `infra/gitops/argocd-application.yaml` och `kubectl apply -f` den. Därefter: `prune` tar bort det som försvinner ur git, `selfHeal` återställer manuella klusteravvikelser.
|
||||
3. **Rollback = `git revert`** av gitops-commiten — Argo CD synkar tillbaka föregående SHA-taggade bilder.
|
||||
4. Repo-variabeln `PLATTFORM_URL` (Settings → Variables) styr webbyggets `VITE_PLATTFORM_URL`/`VITE_AI_ORKESTER_URL`. Hemligheten `felsokning-hemligheter` ligger utanför både git och synken.
|
||||
1. **Publicera** vid varje main-push: bygger de tre bilderna och taggar med git-SHA:t (`GITHUB_TOKEN`, inga externa hemligheter).
|
||||
2. **Driftsätt** startas för hand med en tagg, mot GitHub-miljön `produktion` som kan kräva godkännande. Kör `fmt`, `init`, `validate`, `plan`, `apply`, skriver ut kartan och rökkontrollerar hälsa och API-spec. `bara_plan` visar planen utan att applicera.
|
||||
3. **Rollback** = kör Driftsätt 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 och `prune` tog bort det Terraform skapade.
|
||||
|
||||
Manuell `kubectl apply -k infra/k8s` (avsnittet Driftsätta ovan) fungerar fortfarande för miljöer utan Argo CD.
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
|
||||
| Instrumentavläsning (visual-first) | ✅ Kameran som universellt gränssnitt: `📷 Instrument` i Dokumentera-panelen fotograferar multimetrar, diagnosskärmar, batteritestare m.m. — bildtolkningen identifierar instrumenttyp och extraherar värden/enheter/felkoder med konfidens per värde; teknikern bekräftar innan något loggas. Originalbilden loggas alltid tillsammans med de strukturerade mätvärdena — strukturerad data ersätter aldrig originalevidensen. Ingen integration mot diagnossystem krävs. |
|
||||
| Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. Utskriften går genom ECM-kvalitetsgrinden. |
|
||||
| Märkesspecifika kopplingar | ✅ Verkstaden konfigurerar sina egna OEM-/fordonsdataleverantörer under Inställningar med sina egna credentials ([moduler/markesspecifika-kopplingar.md](moduler/markesspecifika-kopplingar.md)): uppgifterna krypteras med AES-256-GCM i vila, returneras alltid maskerade (`••••3456`) och **alla uppslag görs av servern** — leverantörsnycklar når aldrig webbläsaren. Endast systemadministratören hanterar dem, kopplingarna är organisationsknutna och saknas krypteringsnyckeln sparas ingenting alls (fail closed). Leverantörer är data, inte kod: URL-mall, autentiseringstyp (bearer/header/basic/query) och svarsmappning beskrivs i `integrationer.json` (ConfigMap-utbytbar via `INTEGRATIONER_FIL`) — nya märken läggs till utan ombyggnad. Varje uppslag loggar teststatus, så ett utgånget abonnemang syns i inställningarna i stället för att ge tysta tomma svar. Verifierat i integrationstestet (rollstyrning, maskering, kryptering i databasen, organisationsisolering, fail closed). |
|
||||
| Infrastruktur som kod | ✅ `infra/terraform` är systemets definition ([README](../infra/terraform/README.md)): `karta.tf` beskriver hela systemet en gång som data — tjänster, portar, routing, hemligheter per tjänst, dataflöden och gränser — och `terraform output karta` skriver ut samma sak i klartext. Namnrymden är stängd med nätverkspolicyer (bara ingress→tjänster, plattform→postgres, HTTPS ut utom privata nät), Postgres kör med säkerhetskontext, hemligheter kan genereras eller komma från en secrets-hanterare. Kustomize-/Argo CD-vägen finns kvar men de två ska inte köras mot samma kluster — dokumenterat med rekommendation och bytesväg. |
|
||||
| Infrastruktur som kod | ✅ `infra/terraform` är systemets definition ([README](../infra/terraform/README.md)): `karta.tf` beskriver hela systemet en gång som data — tjänster, portar, routing, hemligheter per tjänst, dataflöden och gränser — och `terraform output karta` skriver ut samma sak i klartext. Namnrymden är stängd med nätverkspolicyer (bara ingress→tjänster, plattform→postgres, HTTPS ut utom privata nät), Postgres kör med säkerhetskontext, hemligheter kan genereras eller komma från en secrets-hanterare. **Databasen har tre lägen** och `databas_lage` saknar standardvärde med flit — valet avgör om det finns säkerhetskopiering: `extern` (managerad Postgres, leverantörens PITR — rekommenderat i produktion), `cnpg` (CloudNativePG i klustret: basbackup 02:30 + WAL-arkivering + failover) och `inbyggd` (en volym, ingen backup, spärrad av en precondition när miljön är produktion). Driftsättning är ett eget CI-flöde som startas för hand med en bildtagg mot en miljö med godkännandekrav, kör plan/apply, skriver ut kartan och rökkontrollerar. Kustomize-/Argo CD-vägen är borttagen. |
|
||||
| Öppet API | ✅ Plattforms-API:t är dokumenterat med OpenAPI 3.0 (`services/plattform/openapi.yaml`) — auth, användare, ärenden/händelser (append-only), översikt, publik delning och AI-orkestern, med scheman för alla händelsetyper. Specen valideras maskinellt, paritetstestas mot serverns rutter och serveras live på `GET /api/openapi.yaml`. |
|
||||
|
||||
## Arkitekturprinciper i koden
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,7 +27,7 @@ su postgres -c "$PGBIN/initdb -D '$DATADIR' -A trust" >/dev/null
|
||||
su postgres -c "$PGBIN/pg_ctl -D '$DATADIR' -o '-p $PGPORT -k /tmp -c listen_addresses=127.0.0.1' -l '$DATADIR/logg' start" >/dev/null
|
||||
su postgres -c "$PGBIN/psql -h /tmp -p $PGPORT -d postgres -qc \"create role plattform login password 'test'\""
|
||||
su postgres -c "$PGBIN/psql -h /tmp -p $PGPORT -d postgres -qc 'create database felsokning owner plattform'"
|
||||
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning -q -f ../../infra/k8s/postgres-init.sql
|
||||
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning -q -f ../../infra/postgres-init.sql
|
||||
|
||||
# ---- Tjänsten upp ----
|
||||
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("AI-orkestern", () => {
|
||||
expect(plattform).toContain("organisation_id");
|
||||
expect(plattform).toContain("arendeIOrg");
|
||||
|
||||
const schema = readFileSync("infra/k8s/postgres-init.sql", "utf8");
|
||||
const schema = readFileSync("infra/postgres-init.sql", "utf8");
|
||||
expect(schema).toContain("before update or delete on felsokning_handelser");
|
||||
expect(schema).toContain("before update or delete on felsokning_arenden");
|
||||
expect(schema).toContain("create table if not exists organisationer");
|
||||
|
||||
Reference in New Issue
Block a user