Files
alva/felsokning/infra/terraform/karta.tf
T
Claude 301c477e25 Merge main: Guidad Felsökning flyttar ur roten till felsokning/
Main är sedan den här grenen skapades en helt annan produkt — Semantika,
en mobilapp med egen CDK-infrastruktur. Den äger nu repots rot: en
npm-workspaces-monorepo med apps/mobile, services/api och infra.

För att båda ska rymmas i samma repo flyttar Guidad Felsökning in i en
egen katalog i stället för att göra anspråk på roten:

  felsokning/app        webbklienten (Vite, egen package.json och
                        eslint-/vitest-konfiguration)
  felsokning/services   plattformstjänsten och AI-orkestern
  felsokning/infra      Terraform och databasschemat
  felsokning/docs       vision, moduler, drift
  felsokning/supabase   edge-funktion och migrationer

Merge:n hade tagit bort 128 filer som Guidad Felsökning bygger på —
värdapplikationens komponenter, Supabase-klienten, tillgångar — eftersom
main raderat dem och den här grenen inte råkat ändra just dem. De är
återställda på sin nya plats. Utan dem gick varken bygget eller
testerna: ai.ts och synk.ts importerar Supabase-klienten.

Semantikas rotfiler är orörda: package.json, eslint.config.js och
.github/workflows/ är deras. Guidad Felsökning har egna motsvarigheter i
sin katalog.

CI flyttar samtidigt från GitHub Actions till .gitea/workflows — samma
syntax, egna runners. .github/workflows/ tillhör Semantika härefter.

Verifierat på den nya platsen: 96 vitest-tester, typkontroll, eslint på
både klient och tjänster, bygge, och integrationstest mot riktig Postgres.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-04 12:22:49 +00:00

151 lines
6.5 KiB
Terraform

# Kartan — hela systemet beskrivet som data.
#
# Resursfilerna läser härifrån i stället för att upprepa portar, namn och
# gränser. Det gör den här filen till ett ställe där man kan läsa av vad
# systemet faktiskt består av, och `terraform output karta` skriver ut
# samma sak i klartext.
locals {
namn = "guidad-felsokning"
etiketter = {
"app.kubernetes.io/part-of" = local.namn
"app.kubernetes.io/managed-by" = "terraform"
"guidad-felsokning/miljo" = var.miljo
}
# Alla tjänster lyssnar internt på 8080 och exponeras som port 80 i
# klustret. Databasen är den enda som inte följer mönstret.
port_container = 8080
port_tjanst = 80
# ---- Tjänsterna -----------------------------------------------------
#
# roll vad den gör
# bild containerbilden
# hemligt vilka hemligheter den får se (allt annat är den blind för)
# utat vem den ringer
tjanster = {
web = {
roll = "Klienten. Statisk SPA bakom oprivilegierad nginx. Innehåller ingen hemlighet — API-adressen bakas in vid bygget."
bild = "${var.register}/${local.namn}-web:${var.bildtagg}"
hemligt = []
utat = ["webbläsaren anropar plattform och orkester direkt över ingressen"]
}
plattform = {
roll = "Backend. Auth, append-only händelse-API, Live Share, organisationsinställningar, ECM-regelpaket, märkesspecifika kopplingar."
bild = "${var.register}/${local.namn}-plattform:${var.bildtagg}"
hemligt = concat(
["jwt-hemlighet", "postgres-losenord", "integration-nyckel"],
var.bilage_lage == "s3" ? ["s3-nyckel-id", "s3-nyckel"] : [],
)
utat = concat(
["postgres:5432", "kundernas leverantörer över internet (spärrat mot privata nät)"],
var.bilage_lage == "s3" ? ["objektlagringen för bilagor"] : [],
)
}
orkester = {
roll = "AI-orkestern. Routar per uppgift till Claude, äger systemprompt och svarsschema. Verifierar plattformens JWT."
bild = "${var.register}/${local.namn}-ai-orkester:${var.bildtagg}"
hemligt = ["anthropic-api-nyckel", "jwt-hemlighet"]
utat = ["api.anthropic.com"]
}
postgres = {
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 = var.databas_lage == "cnpg" ? ["objektlagringen för backup och WAL-arkiv"] : []
}
}
# ---- Routing --------------------------------------------------------
#
# Längsta prefix vinner. Ordningen här är den ordning ingressen får
# reglerna i.
routing = [
{ prefix = "/api/ai", till = "ai-orkester", varfor = "AI-anrop går till orkestern, aldrig via plattformen" },
{ prefix = "/api", till = "plattform", varfor = "allt övrigt API" },
{ prefix = "/halsa", till = "plattform", varfor = "hälsokontroll" },
{ prefix = "/", till = "web", varfor = "klienten" },
]
# ---- Hemligheterna --------------------------------------------------
#
# En enda Secret, men varje tjänst monterar bara sina egna nycklar.
hemligheter = {
"anthropic-api-key" = var.anthropic_api_nyckel
"jwt-secret" = coalesce(var.jwt_hemlighet, random_password.jwt.result)
"postgres-losenord" = coalesce(var.postgres_losenord, random_password.postgres.result)
"integration-nyckel" = coalesce(
var.integration_nyckel,
random_id.integration_nyckel.hex,
)
"s3-nyckel-id" = var.s3_nyckel_id
"s3-nyckel" = var.s3_nyckel
}
# ---- Dataflöden -----------------------------------------------------
#
# Det som är värt att förstå innan man ändrar något: vad som rör sig
# var, och var gränserna går.
dataflode = [
"Tekniker → web → ingress → plattform → postgres all ärendedata, append-only",
"Tekniker → web → ingress → orkester → Claude underlag ut, klassificerade svar in",
"Kund → ingress → plattform → postgres Live Share: bara tillåtna händelsetyper",
"Plattform → kundens leverantör VIN/regnr ut, fordonsuppgifter in",
]
bilagor = {
lage = var.bilage_lage
var = var.bilage_lage == "s3" ? "${var.s3.endpoint}/${var.s3.hink}/${var.s3.prefix}" : "tabellen bilage_innehall"
hur = join(" ", [
"Innehållet ligger utanför händelsen; loggen bär referensen och innehållets SHA-256.",
"Innehållsadresserat, så samma foto lagras en gång.",
"Hashen kontrolleras vid utlämning — en utbytt bild lämnas inte ut.",
])
}
granser = [
"Organisationsgränsen: varje fråga mot ärendedata filtreras på organisation_id i SQL:en, inte i klienten.",
"Delningsgränsen: tillåtelselista över händelsetyper per nivå (kund/partner/intern) — nya typer är interna tills de aktivt släpps fram.",
"Hemlighetsgränsen: Claude-nyckeln och kundernas leverantörsnycklar finns bara serversidan. Klienten ser maskerade värden.",
"Historikgränsen: append-only i både API och databas (triggers). Ingen roll kan ändra eller radera en händelse.",
"Bilagegränsen: en bilaga kan bara hämtas via en delningslänk om händelsen den hör till är synlig på den nivån.",
]
# ---- Det som medvetet inte ingår ------------------------------------
avgransningar = concat(
var.databas_lage == "inbyggd" ? ["INGEN SÄKERHETSKOPIERING — läget inbyggd har en volym och inget mer. Endast prov och demo."] : [],
[
"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.
resource "random_password" "jwt" {
length = 64
special = false
}
resource "random_password" "postgres" {
length = 32
special = false
}
resource "random_id" "integration_nyckel" {
byte_length = 32
}