Files
alva/infra/terraform/70-natverk.tf
T
Claude 19796bec70 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
2026-08-03 22:16:52 +00:00

267 lines
5.9 KiB
Terraform

# Vem får prata med vem.
#
# Utan nätverkspolicyer är ett Kubernetes-kluster platt: vilken pod som
# helst når databasen på 5432. Här stängs allt först, och bara de
# faktiska flödena öppnas — samma flöden som står i local.dataflode.
#
# Kräver en CNI som tillämpar NetworkPolicy (Cilium, Calico, Antrea).
# Med en CNI som inte gör det är de här reglerna dokumentation, inte
# skydd — kontrollera det innan ni litar på dem.
# Grundregel: ingen inkommande trafik alls i namnrymden.
resource "kubernetes_network_policy_v1" "neka_allt_in" {
metadata {
name = "neka-allt-inkommande"
namespace = kubernetes_namespace_v1.denna.metadata[0].name
labels = local.etiketter
}
spec {
pod_selector {}
policy_types = ["Ingress"]
}
}
# 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
labels = local.etiketter
}
spec {
pod_selector {
match_labels = { app = "postgres" }
}
policy_types = ["Ingress"]
ingress {
from {
pod_selector {
match_labels = { app = "plattform" }
}
}
ports {
port = "5432"
protocol = "TCP"
}
}
}
}
# De tre webbtjänsterna: bara från ingress-kontrollern.
resource "kubernetes_network_policy_v1" "tjanster_in" {
for_each = toset(["web", "plattform", "ai-orkester"])
metadata {
name = "${each.key}-endast-fran-ingress"
namespace = kubernetes_namespace_v1.denna.metadata[0].name
labels = local.etiketter
}
spec {
pod_selector {
match_labels = { app = each.key }
}
policy_types = ["Ingress"]
ingress {
from {
namespace_selector {
match_labels = {
"kubernetes.io/metadata.name" = var.ingress_namnrymd
}
}
}
ports {
port = tostring(local.port_container)
protocol = "TCP"
}
}
}
}
# Utgående: DNS åt alla, i övrigt bara det som faktiskt behövs.
resource "kubernetes_network_policy_v1" "dns_ut" {
metadata {
name = "dns-ut"
namespace = kubernetes_namespace_v1.denna.metadata[0].name
labels = local.etiketter
}
spec {
pod_selector {}
policy_types = ["Egress"]
egress {
ports {
port = "53"
protocol = "UDP"
}
ports {
port = "53"
protocol = "TCP"
}
}
}
}
# Klienten är statisk och ringer ingenting från podden.
resource "kubernetes_network_policy_v1" "web_ut" {
metadata {
name = "web-inget-utgaende"
namespace = kubernetes_namespace_v1.denna.metadata[0].name
labels = local.etiketter
}
spec {
pod_selector {
match_labels = { app = "web" }
}
policy_types = ["Egress"]
# Ingen egress-regel = inget utgående (utöver DNS-policyn ovan).
}
}
# 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
labels = local.etiketter
}
spec {
pod_selector {
match_labels = { app = "postgres" }
}
policy_types = ["Egress"]
}
}
# Plattformen: databasen internt, och HTTPS ut till kundernas
# leverantörer. Klustrets och molnets interna adresser är undantagna —
# samma gräns som koden själv upprätthåller (pekarInat), här en gång till
# på nätverksnivå. Två oberoende spärrar för samma sak.
resource "kubernetes_network_policy_v1" "plattform_ut" {
metadata {
name = "plattform-utgaende"
namespace = kubernetes_namespace_v1.denna.metadata[0].name
labels = local.etiketter
}
spec {
pod_selector {
match_labels = { app = "plattform" }
}
policy_types = ["Egress"]
# 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]
content {
to {
pod_selector {
match_labels = local.inbyggd_databas ? { app = "postgres" } : { "cnpg.io/cluster" = "felsokning-db" }
}
}
ports {
port = "5432"
protocol = "TCP"
}
}
}
egress {
to {
ip_block {
cidr = "0.0.0.0/0"
except = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16", # molnets metadatatjänst
"127.0.0.0/8",
"100.64.0.0/10",
]
}
}
ports {
port = "443"
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"
}
}
}
}
}
# Orkestern: bara HTTPS ut till Claude.
resource "kubernetes_network_policy_v1" "orkester_ut" {
metadata {
name = "ai-orkester-utgaende"
namespace = kubernetes_namespace_v1.denna.metadata[0].name
labels = local.etiketter
}
spec {
pod_selector {
match_labels = { app = "ai-orkester" }
}
policy_types = ["Egress"]
egress {
to {
ip_block {
cidr = "0.0.0.0/0"
except = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"127.0.0.0/8",
]
}
}
ports {
port = "443"
protocol = "TCP"
}
}
}
}