# Plattformstjänsten — systemets mitt. # # Äger: inloggning (bcrypt via pgcrypto, HS256-JWT med roll + organisation # i anspråken), append-only händelse-API, Live Share med serverstyrd # filtrering, organisationsinställningar, ECM-regelpaketet och de # märkesspecifika kopplingarna. # # Ser tre hemligheter och inga fler: JWT-hemligheten (delas med # orkestern), databaslösenordet och krypteringsnyckeln för kundernas # leverantörsuppgifter. resource "kubernetes_deployment_v1" "plattform" { metadata { name = "plattform" namespace = kubernetes_namespace_v1.denna.metadata[0].name labels = merge(local.etiketter, { app = "plattform" }) } spec { replicas = var.repliker.plattform selector { match_labels = { app = "plattform" } } template { metadata { labels = merge(local.etiketter, { app = "plattform" }) } spec { # Behöver token: IRSA hämtar AWS-uppgifter genom det. service_account_name = kubernetes_service_account_v1.plattform.metadata[0].name security_context { run_as_non_root = true seccomp_profile { type = "RuntimeDefault" } } container { name = "plattform" image = local.tjanster.plattform.bild port { container_port = local.port_container } # Alla hemligheter kommer ur den spegling External Secrets # gör från Secrets Manager. Ingen av dem finns i Terraforms # tillstånd eller i något manifest. env_from { secret_ref { name = "felsokning-hemligheter" } } # Klienten serveras från samma domän som API:t, så CORS behöver # inte vara öppet. env { name = "TILLATNA_URSPRUNG" value = "https://${local.aws.doman}" } env { name = "REGISTRERING_OPPEN" value = var.registrering_oppen ? "true" : "false" } env { name = "TILLAT_INTERNA_UPPSLAG" value = var.tillat_interna_uppslag ? "true" : "false" } # Bilagorna ligger i S3. Inga nycklar behövs — tjänsten # signerar med tjänstekontots roll via IRSA. env { name = "BILAGE_LAGE" value = "s3" } env { name = "S3_HINK" value = local.aws.bilage_hink } env { name = "S3_REGION" value = local.aws.region } env { name = "S3_ENDPOINT" value = "https://s3.${local.aws.region}.amazonaws.com" } resources { requests = { cpu = "100m", memory = "128Mi" } limits = { cpu = "1", memory = "512Mi" } } readiness_probe { http_get { path = "/halsa" port = local.port_container } initial_delay_seconds = 3 period_seconds = 10 } liveness_probe { http_get { path = "/halsa" port = local.port_container } period_seconds = 15 } security_context { allow_privilege_escalation = false read_only_root_filesystem = true capabilities { drop = ["ALL"] } } } } } } } resource "kubernetes_service_v1" "plattform" { metadata { name = "plattform" namespace = kubernetes_namespace_v1.denna.metadata[0].name labels = merge(local.etiketter, { app = "plattform" }) } spec { selector = { app = "plattform" } port { port = local.port_tjanst target_port = local.port_container } } } resource "kubernetes_horizontal_pod_autoscaler_v2" "plattform" { metadata { name = "plattform" namespace = kubernetes_namespace_v1.denna.metadata[0].name labels = local.etiketter } spec { min_replicas = var.repliker.plattform max_replicas = var.max_repliker.plattform scale_target_ref { api_version = "apps/v1" kind = "Deployment" name = kubernetes_deployment_v1.plattform.metadata[0].name } metric { type = "Resource" resource { name = "cpu" target { type = "Utilization" average_utilization = 70 } } } } } resource "kubernetes_pod_disruption_budget_v1" "plattform" { metadata { name = "plattform" namespace = kubernetes_namespace_v1.denna.metadata[0].name labels = local.etiketter } spec { min_available = 1 selector { match_labels = { app = "plattform" } } } } # ---- Gallringen (ALVA-PROC-0040 · TÜV T-4) ----------------------------- # # Gallringsdatumet räknades fram vid avslut och lästes sedan aldrig av # någon. Lagringsbegränsningen var en nedskriven avsikt, inte en # verkställd kontroll — och eftersom ingenting konsumerade värdet märkte # heller ingen att det under en period inte ens gick att skriva. # # Egen körning och inte en timer inne i webbtjänsten: en gallring som # avbryts av en omstart mitt i ska kunna köras om, och den ska synas som # en egen körning i driften i stället för att gömma sig i en podd som # samtidigt betjänar trafik. resource "kubernetes_cron_job_v1" "gallring" { metadata { name = "gallring" namespace = kubernetes_namespace_v1.denna.metadata[0].name labels = merge(local.etiketter, { app = "gallring" }) } spec { # Nattetid, en gång per dygn. Gallring är aldrig brådskande — ett # dygns fördröjning är oproblematiskt, en för tidig gallring är # oåterkallelig. schedule = "17 2 * * *" concurrency_policy = "Forbid" successful_jobs_history_limit = 3 failed_jobs_history_limit = 3 starting_deadline_seconds = 3600 job_template { metadata { labels = merge(local.etiketter, { app = "gallring" }) } spec { # Misslyckas den lämnas nycklarna kvar. Det försiktiga utfallet: # för tidig gallring går inte att ångra, en försenad gör det. backoff_limit = 2 template { metadata { labels = merge(local.etiketter, { app = "gallring" }) } spec { restart_policy = "Never" service_account_name = kubernetes_service_account_v1.plattform.metadata[0].name security_context { run_as_non_root = true seccomp_profile { type = "RuntimeDefault" } } container { name = "gallring" image = local.tjanster.plattform.bild command = ["node", "gallring.mjs"] # Samma hemligheter som tjänsten: DATABASE_URL och # PERSONNYCKEL_HUVUD behövs, inget annat. env_from { secret_ref { name = "felsokning-hemligheter" } } resources { requests = { cpu = "50m" memory = "128Mi" } limits = { memory = "256Mi" } } } } } } } } } # ---- Månadsfaktureringen (ALVA-PROC-0002) ------------------------------ # # Egen körning av samma skäl som gallringen: en körning som avbryts mitt i # ska kunna köras om, och den ska synas som en egen körning i driften. # # Jobbet är idempotent på perioden — andra körningen samma dygn fakturerar # ingenting. Det är viktigare än det låter: ett fakturajobb som # dubbelfakturerar vid en omstart är värre än ett som inte kör alls. resource "kubernetes_cron_job_v1" "manadsfakturering" { metadata { name = "manadsfakturering" namespace = kubernetes_namespace_v1.denna.metadata[0].name labels = merge(local.etiketter, { app = "manadsfakturering" }) } spec { # Dagligen. Perioden avgör vem som faktureras, inte schemat — en # organisation som registrerade sig den 20:e faktureras den 20:e. schedule = "40 3 * * *" concurrency_policy = "Forbid" successful_jobs_history_limit = 7 failed_jobs_history_limit = 7 starting_deadline_seconds = 3600 job_template { metadata { labels = merge(local.etiketter, { app = "manadsfakturering" }) } spec { backoff_limit = 2 template { metadata { labels = merge(local.etiketter, { app = "manadsfakturering" }) } spec { restart_policy = "Never" service_account_name = kubernetes_service_account_v1.plattform.metadata[0].name security_context { run_as_non_root = true seccomp_profile { type = "RuntimeDefault" } } container { name = "manadsfakturering" image = local.tjanster.plattform.bild command = ["node", "manadsfakturering.mjs"] env_from { secret_ref { name = "felsokning-hemligheter" } } resources { requests = { cpu = "50m" memory = "128Mi" } limits = { memory = "256Mi" } } } } } } } } }