19796bec70
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
216 lines
5.1 KiB
Terraform
216 lines
5.1 KiB
Terraform
# 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 {
|
|
automount_service_account_token = false
|
|
|
|
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
|
|
}
|
|
|
|
env {
|
|
name = "JWT_SECRET"
|
|
|
|
value_from {
|
|
secret_key_ref {
|
|
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
|
key = "jwt-secret"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Krypterar kundernas leverantörsuppgifter i vila. Saknas den
|
|
# sparas ingenting alls — tjänsten failar closed i stället för
|
|
# att lagra i klartext.
|
|
env {
|
|
name = "INTEGRATION_NYCKEL"
|
|
|
|
value_from {
|
|
secret_key_ref {
|
|
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
|
key = "integration-nyckel"
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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]
|
|
|
|
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 = local.databas_anslutning
|
|
}
|
|
|
|
# Klienten serveras från samma domän som API:t, så CORS behöver
|
|
# inte vara öppet.
|
|
env {
|
|
name = "TILLATNA_URSPRUNG"
|
|
value = "https://${var.doman}"
|
|
}
|
|
|
|
env {
|
|
name = "REGISTRERING_OPPEN"
|
|
value = var.registrering_oppen ? "true" : "false"
|
|
}
|
|
|
|
env {
|
|
name = "TILLAT_INTERNA_UPPSLAG"
|
|
value = var.tillat_interna_uppslag ? "true" : "false"
|
|
}
|
|
|
|
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" }
|
|
}
|
|
}
|
|
}
|