Arbetslasten kopplad till AWS — kedjan är helt egen
Andra lagret läser nu basens utdata i stället för att upprepa någonting. 05-aws.tf hämtar klustret, domänen, certifikatet, registret, rollerna, hinken och hemligheternas namn — ändras något i basen slår det igenom här utan att en rad ändras. Autentiseringen mot klustret sker med en färsk EKS-token i stället för en kubeconfig på disk: inget att distribuera, inget som går ut. Hemligheterna bor i Secrets Manager och speglas in av External Secrets var timme. Terraform ser aldrig värdena, och det är själva poängen — en hemlighet som passerar Terraform hamnar i tillståndsfilen. Roteras en hemlighet följer klustret efter av sig självt. Plattformstjänsten kör som ett tjänstekonto med IRSA. Rollen är bunden till exakt det kontot i den namnrymden, så grannpodden på samma nod får ingenting på köpet. Bilagorna signeras mot S3 med samma roll — inga nycklar existerar att läcka. Postgres-podden är borta. Händelseloggen ligger i Aurora utanför klustret, i ett subnätlager utan routing ut, med automatisk säkerhetskopiering och PITR. Ingressen är en ALB med ACM-certifikat i stället för nginx, och alla tre tjänsterna delar den genom samma gruppnamn. Gitea med egna Actions-runners kör i samma kluster. Därmed finns inget externt beroende kvar: källkod, bygge, register och driftsättning ligger allt i vår AWS-miljö. Runnern och driftsättningen har varsitt tjänstekonto med skilda roller — bygget får publicera till ECR men inte röra klustret, driften tvärtom. Ett komprometterat bygge kan inte driftsätta. Gitea kör SQLite på en EBS-volym i stället för en egen Aurora-databas. En verkstadsinstallation har en handfull utvecklare, och ytterligare en databas att säkerhetskopiera och övervaka är kostnad utan motsvarande nytta. Verifierat: 96 vitest-tester, rotens lint och format:check, terraform fmt på båda lagren och statisk referenskontroll av vardera (88 respektive 35 resurser, inga dinglande referenser, alla count-resurser indexerade). terraform validate kunde inte köras här — registry.terraform.io är blockerad av sessionens egress-policy — så CI-jobbet kör den. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# Kopplingen till AWS-basen.
|
||||||
|
#
|
||||||
|
# Allt som basen redan bestämt läses här i stället för att upprepas:
|
||||||
|
# klustrets namn och certifikat, domänen, certifikatet, registret,
|
||||||
|
# rollerna, hinken och hemligheternas namn. Ändras något i basen slår
|
||||||
|
# det igenom här utan att en enda rad ändras.
|
||||||
|
|
||||||
|
data "terraform_remote_state" "bas" {
|
||||||
|
backend = "s3"
|
||||||
|
|
||||||
|
config = {
|
||||||
|
bucket = var.tillstandshink
|
||||||
|
key = "aws/terraform.tfstate"
|
||||||
|
region = var.region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
aws = data.terraform_remote_state.bas.outputs.for_arbetslast
|
||||||
|
|
||||||
|
# På AWS är valen redan gjorda: databasen är Aurora med
|
||||||
|
# säkerhetskopiering och PITR, bilagorna ligger i S3. De inbyggda
|
||||||
|
# lägena finns kvar i koden för lokal provkörning, men här är de
|
||||||
|
# aldrig aktuella.
|
||||||
|
databas_lage = "extern"
|
||||||
|
bilage_lage = "s3"
|
||||||
|
|
||||||
|
inbyggd_databas = false
|
||||||
|
cnpg_databas = false
|
||||||
|
extern_databas = true
|
||||||
|
|
||||||
|
# Plattformstjänsten läser DATABASE_URL. Värdet kommer ur den
|
||||||
|
# synkade hemligheten, inte härifrån — se 30-plattform.tf.
|
||||||
|
databas_anslutning = "" # sätts via secretKeyRef
|
||||||
|
}
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
# Namnrymden och den enda hemligheten.
|
# Namnrymden, tjänstekontot och hemligheterna.
|
||||||
|
#
|
||||||
|
# Hemligheterna bor i AWS Secrets Manager, inte här. External Secrets
|
||||||
|
# speglar dem in i klustret så att podarna kan läsa dem som vanliga
|
||||||
|
# miljövariabler — men sanningskällan är Secrets Manager, med rotation,
|
||||||
|
# revisionsspår per läsning och kryptering med egen KMS-nyckel.
|
||||||
|
#
|
||||||
|
# Terraform ser aldrig värdena, och det är själva poängen: en hemlighet
|
||||||
|
# som passerar Terraform hamnar i tillståndsfilen.
|
||||||
|
|
||||||
resource "kubernetes_namespace_v1" "denna" {
|
resource "kubernetes_namespace_v1" "denna" {
|
||||||
metadata {
|
metadata {
|
||||||
@@ -7,37 +15,101 @@ resource "kubernetes_namespace_v1" "denna" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# En Secret, men varje tjänst monterar bara sina egna nycklar — se
|
# Tjänstekontot plattformstjänsten kör som. IRSA-annoteringen binder det
|
||||||
# hemligt-fältet per tjänst i karta.tf.
|
# till rollen basen skapade — och rollen är i sin tur bunden till exakt
|
||||||
#
|
# det här kontot i den här namnrymden. Ingen annan pod kan anta den.
|
||||||
# Vill ni hellre ha hemligheterna utanför Terraform-tillståndet: byt den
|
resource "kubernetes_service_account_v1" "plattform" {
|
||||||
# här resursen mot ett ExternalSecret och peka manifesten på samma namn.
|
|
||||||
resource "kubernetes_secret_v1" "hemligheter" {
|
|
||||||
metadata {
|
metadata {
|
||||||
name = "felsokning-hemligheter"
|
name = "plattform"
|
||||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||||
labels = local.etiketter
|
labels = local.etiketter
|
||||||
}
|
|
||||||
|
|
||||||
type = "Opaque"
|
annotations = {
|
||||||
data = local.hemligheter
|
"eks.amazonaws.com/role-arn" = local.aws.plattform_roll
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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
|
|
||||||
labels = local.etiketter
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"init.sql" = file("${path.module}/../postgres-init.sql")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Var hemligheterna hämtas ifrån. Autentiseringen sker med
|
||||||
|
# tjänstekontots roll — inga nycklar att distribuera.
|
||||||
|
resource "kubernetes_manifest" "hemlighetskalla" {
|
||||||
|
manifest = {
|
||||||
|
apiVersion = "external-secrets.io/v1beta1"
|
||||||
|
kind = "SecretStore"
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
name = "aws"
|
||||||
|
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = {
|
||||||
|
provider = {
|
||||||
|
aws = {
|
||||||
|
service = "SecretsManager"
|
||||||
|
region = local.aws.region
|
||||||
|
|
||||||
|
auth = {
|
||||||
|
jwt = {
|
||||||
|
serviceAccountRef = {
|
||||||
|
name = kubernetes_service_account_v1.plattform.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.external_secrets]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Speglingen. Namnen till vänster är miljövariablerna tjänsterna läser;
|
||||||
|
# till höger nycklarna i Secrets Manager.
|
||||||
|
resource "kubernetes_manifest" "hemligheter" {
|
||||||
|
manifest = {
|
||||||
|
apiVersion = "external-secrets.io/v1beta1"
|
||||||
|
kind = "ExternalSecret"
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
name = "felsokning-hemligheter"
|
||||||
|
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec = {
|
||||||
|
# Roteras en hemlighet i Secrets Manager följer klustret efter inom
|
||||||
|
# en timme, utan att någon behöver göra något.
|
||||||
|
refreshInterval = "1h"
|
||||||
|
|
||||||
|
secretStoreRef = {
|
||||||
|
name = "aws"
|
||||||
|
kind = "SecretStore"
|
||||||
|
}
|
||||||
|
|
||||||
|
target = {
|
||||||
|
name = "felsokning-hemligheter"
|
||||||
|
creationPolicy = "Owner"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
secretKey = "DATABASE_URL"
|
||||||
|
remoteRef = { key = local.aws.hemlighet_databas, property = "url" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey = "ANTHROPIC_API_KEY"
|
||||||
|
remoteRef = { key = local.aws.hemlighet_app, property = "anthropic_api_key" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey = "JWT_SECRET"
|
||||||
|
remoteRef = { key = local.aws.hemlighet_app, property = "jwt_secret" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey = "INTEGRATION_NYCKEL"
|
||||||
|
remoteRef = { key = local.aws.hemlighet_app, property = "integration_nyckel" }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [kubernetes_manifest.hemlighetskalla]
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Det klustret behöver innan vår egen arbetslast är meningsfull.
|
||||||
|
#
|
||||||
|
# Tre stycken, och alla tre finns av ett skäl som är synligt i systemet:
|
||||||
|
# ALB-kontrollern gör Ingress till en riktig lastbalanserare
|
||||||
|
# External Secrets hämtar hemligheter ur Secrets Manager
|
||||||
|
# metrics-server autoskalningen har inget att skala på utan den
|
||||||
|
|
||||||
|
resource "helm_release" "lastbalanserare" {
|
||||||
|
name = "aws-load-balancer-controller"
|
||||||
|
repository = "https://aws.github.io/eks-charts"
|
||||||
|
chart = "aws-load-balancer-controller"
|
||||||
|
version = "1.10.1"
|
||||||
|
namespace = "kube-system"
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "clusterName"
|
||||||
|
value = local.aws.kluster
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "serviceAccount.create"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "serviceAccount.name"
|
||||||
|
value = "aws-load-balancer-controller"
|
||||||
|
}
|
||||||
|
|
||||||
|
# IRSA: kontrollern antar rollen basen skapade. Rollen är bunden till
|
||||||
|
# exakt det här tjänstekontot i den här namnrymden.
|
||||||
|
set {
|
||||||
|
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
|
||||||
|
value = local.aws.lastbalanserar_roll
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "region"
|
||||||
|
value = local.aws.region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "helm_release" "external_secrets" {
|
||||||
|
name = "external-secrets"
|
||||||
|
repository = "https://charts.external-secrets.io"
|
||||||
|
chart = "external-secrets"
|
||||||
|
version = "0.10.7"
|
||||||
|
namespace = "external-secrets"
|
||||||
|
create_namespace = true
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "installCRDs"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "helm_release" "metrics" {
|
||||||
|
name = "metrics-server"
|
||||||
|
repository = "https://kubernetes-sigs.github.io/metrics-server/"
|
||||||
|
chart = "metrics-server"
|
||||||
|
version = "3.12.2"
|
||||||
|
namespace = "kube-system"
|
||||||
|
}
|
||||||
|
|
||||||
|
# CloudWatch-agenten: containerloggar och Container Insights. Det är
|
||||||
|
# den som gör larmet på nodantal i basen meningsfullt — utan agenten
|
||||||
|
# finns mätvärdet inte.
|
||||||
|
resource "helm_release" "cloudwatch" {
|
||||||
|
name = "aws-cloudwatch-metrics"
|
||||||
|
repository = "https://aws.github.io/eks-charts"
|
||||||
|
chart = "aws-cloudwatch-metrics"
|
||||||
|
version = "0.0.11"
|
||||||
|
namespace = "amazon-cloudwatch"
|
||||||
|
create_namespace = true
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "clusterName"
|
||||||
|
value = local.aws.kluster
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
# 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.
|
|
||||||
#
|
|
||||||
# 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 = var.bilage_lage != "s3" || (var.s3.endpoint != "" && var.s3.hink != "" && var.s3.region != "" && var.s3_nyckel_id != "" && var.s3_nyckel != "")
|
|
||||||
error_message = "bilage_lage = \"s3\" kräver s3.endpoint, s3.hink, s3.region, s3_nyckel_id och s3_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
|
|
||||||
labels = merge(local.etiketter, { app = "postgres" })
|
|
||||||
}
|
|
||||||
|
|
||||||
spec {
|
|
||||||
service_name = "postgres"
|
|
||||||
replicas = 1
|
|
||||||
|
|
||||||
selector {
|
|
||||||
match_labels = { app = "postgres" }
|
|
||||||
}
|
|
||||||
|
|
||||||
template {
|
|
||||||
metadata {
|
|
||||||
labels = merge(local.etiketter, { app = "postgres" })
|
|
||||||
}
|
|
||||||
|
|
||||||
spec {
|
|
||||||
automount_service_account_token = false
|
|
||||||
|
|
||||||
security_context {
|
|
||||||
run_as_non_root = true
|
|
||||||
run_as_user = 70 # postgres i alpine-bilden
|
|
||||||
fs_group = 70
|
|
||||||
|
|
||||||
seccomp_profile {
|
|
||||||
type = "RuntimeDefault"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container {
|
|
||||||
name = "postgres"
|
|
||||||
image = local.tjanster.postgres.bild
|
|
||||||
|
|
||||||
port {
|
|
||||||
container_port = 5432
|
|
||||||
}
|
|
||||||
|
|
||||||
env {
|
|
||||||
name = "POSTGRES_DB"
|
|
||||||
value = "felsokning"
|
|
||||||
}
|
|
||||||
|
|
||||||
env {
|
|
||||||
name = "POSTGRES_USER"
|
|
||||||
value = "plattform"
|
|
||||||
}
|
|
||||||
|
|
||||||
env {
|
|
||||||
name = "POSTGRES_PASSWORD"
|
|
||||||
|
|
||||||
value_from {
|
|
||||||
secret_key_ref {
|
|
||||||
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
|
||||||
key = "postgres-losenord"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
env {
|
|
||||||
name = "PGDATA"
|
|
||||||
value = "/var/lib/postgresql/data/pgdata"
|
|
||||||
}
|
|
||||||
|
|
||||||
volume_mount {
|
|
||||||
name = "data"
|
|
||||||
mount_path = "/var/lib/postgresql/data"
|
|
||||||
}
|
|
||||||
|
|
||||||
volume_mount {
|
|
||||||
name = "init"
|
|
||||||
mount_path = "/docker-entrypoint-initdb.d"
|
|
||||||
}
|
|
||||||
|
|
||||||
resources {
|
|
||||||
requests = { cpu = "250m", memory = "512Mi" }
|
|
||||||
limits = { cpu = "2", memory = "2Gi" }
|
|
||||||
}
|
|
||||||
|
|
||||||
readiness_probe {
|
|
||||||
exec {
|
|
||||||
command = ["pg_isready", "-U", "plattform", "-d", "felsokning"]
|
|
||||||
}
|
|
||||||
|
|
||||||
initial_delay_seconds = 5
|
|
||||||
period_seconds = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
# Startprob i stället för liveness under uppstart: en stor
|
|
||||||
# återställning får ta tid utan att poden dödas i loop.
|
|
||||||
startup_probe {
|
|
||||||
exec {
|
|
||||||
command = ["pg_isready", "-U", "plattform", "-d", "felsokning"]
|
|
||||||
}
|
|
||||||
|
|
||||||
period_seconds = 10
|
|
||||||
failure_threshold = 30
|
|
||||||
}
|
|
||||||
|
|
||||||
security_context {
|
|
||||||
allow_privilege_escalation = false
|
|
||||||
|
|
||||||
capabilities {
|
|
||||||
drop = ["ALL"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
volume {
|
|
||||||
name = "init"
|
|
||||||
|
|
||||||
config_map {
|
|
||||||
name = kubernetes_config_map_v1.postgres_init[0].metadata[0].name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
volume_claim_template {
|
|
||||||
metadata {
|
|
||||||
name = "data"
|
|
||||||
}
|
|
||||||
|
|
||||||
spec {
|
|
||||||
access_modes = ["ReadWriteOnce"]
|
|
||||||
storage_class_name = var.lagringsklass != "" ? var.lagringsklass : null
|
|
||||||
|
|
||||||
resources {
|
|
||||||
requests = { storage = var.databas_storlek }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "kubernetes_service_v1" "postgres" {
|
|
||||||
count = local.inbyggd_databas ? 1 : 0
|
|
||||||
|
|
||||||
metadata {
|
|
||||||
name = "postgres"
|
|
||||||
namespace = kubernetes_namespace_v1.denna.metadata[0].name
|
|
||||||
labels = merge(local.etiketter, { app = "postgres" })
|
|
||||||
}
|
|
||||||
|
|
||||||
spec {
|
|
||||||
selector = { app = "postgres" }
|
|
||||||
|
|
||||||
port {
|
|
||||||
port = 5432
|
|
||||||
target_port = 5432
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,8 @@ resource "kubernetes_deployment_v1" "plattform" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
spec {
|
spec {
|
||||||
automount_service_account_token = false
|
# Behöver token: IRSA hämtar AWS-uppgifter genom det.
|
||||||
|
service_account_name = kubernetes_service_account_v1.plattform.metadata[0].name
|
||||||
|
|
||||||
security_context {
|
security_context {
|
||||||
run_as_non_root = true
|
run_as_non_root = true
|
||||||
@@ -47,58 +48,20 @@ resource "kubernetes_deployment_v1" "plattform" {
|
|||||||
container_port = local.port_container
|
container_port = local.port_container
|
||||||
}
|
}
|
||||||
|
|
||||||
env {
|
# Alla hemligheter kommer ur den spegling External Secrets
|
||||||
name = "JWT_SECRET"
|
# gör från Secrets Manager. Ingen av dem finns i Terraforms
|
||||||
|
# tillstånd eller i något manifest.
|
||||||
value_from {
|
env_from {
|
||||||
secret_key_ref {
|
secret_ref {
|
||||||
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
name = "felsokning-hemligheter"
|
||||||
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
|
# Klienten serveras från samma domän som API:t, så CORS behöver
|
||||||
# inte vara öppet.
|
# inte vara öppet.
|
||||||
env {
|
env {
|
||||||
name = "TILLATNA_URSPRUNG"
|
name = "TILLATNA_URSPRUNG"
|
||||||
value = "https://${var.doman}"
|
value = "https://${local.aws.doman}"
|
||||||
}
|
}
|
||||||
|
|
||||||
env {
|
env {
|
||||||
@@ -111,44 +74,29 @@ resource "kubernetes_deployment_v1" "plattform" {
|
|||||||
value = var.tillat_interna_uppslag ? "true" : "false"
|
value = var.tillat_interna_uppslag ? "true" : "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Bilagornas innehåll: i databasen eller i objektlagring.
|
# Bilagorna ligger i S3. Inga nycklar behövs — tjänsten
|
||||||
|
# signerar med tjänstekontots roll via IRSA.
|
||||||
env {
|
env {
|
||||||
name = "BILAGE_LAGE"
|
name = "BILAGE_LAGE"
|
||||||
value = var.bilage_lage
|
value = "s3"
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic "env" {
|
env {
|
||||||
for_each = var.bilage_lage == "s3" ? {
|
name = "S3_HINK"
|
||||||
S3_ENDPOINT = var.s3.endpoint
|
value = local.aws.bilage_hink
|
||||||
S3_HINK = var.s3.hink
|
|
||||||
S3_REGION = var.s3.region
|
|
||||||
S3_PREFIX = var.s3.prefix
|
|
||||||
} : {}
|
|
||||||
|
|
||||||
content {
|
|
||||||
name = env.key
|
|
||||||
value = env.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic "env" {
|
env {
|
||||||
for_each = var.bilage_lage == "s3" ? {
|
name = "S3_REGION"
|
||||||
S3_NYCKEL_ID = "s3-nyckel-id"
|
value = local.aws.region
|
||||||
S3_NYCKEL = "s3-nyckel"
|
|
||||||
} : {}
|
|
||||||
|
|
||||||
content {
|
|
||||||
name = env.key
|
|
||||||
|
|
||||||
value_from {
|
|
||||||
secret_key_ref {
|
|
||||||
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
|
||||||
key = env.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "S3_ENDPOINT"
|
||||||
|
value = "https://s3.${local.aws.region}.amazonaws.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
resources {
|
resources {
|
||||||
requests = { cpu = "100m", memory = "128Mi" }
|
requests = { cpu = "100m", memory = "128Mi" }
|
||||||
limits = { cpu = "1", memory = "512Mi" }
|
limits = { cpu = "1", memory = "512Mi" }
|
||||||
|
|||||||
@@ -48,13 +48,17 @@ resource "kubernetes_deployment_v1" "orkester" {
|
|||||||
container_port = local.port_container
|
container_port = local.port_container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Claude-nyckeln och JWT-hemligheten kommer ur speglingen
|
||||||
|
# från Secrets Manager — samma secret som plattformen läser,
|
||||||
|
# men orkestern får bara de två nycklar den behöver genom att
|
||||||
|
# den inte känner till de andra namnen.
|
||||||
env {
|
env {
|
||||||
name = "ANTHROPIC_API_KEY"
|
name = "ANTHROPIC_API_KEY"
|
||||||
|
|
||||||
value_from {
|
value_from {
|
||||||
secret_key_ref {
|
secret_key_ref {
|
||||||
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
name = "felsokning-hemligheter"
|
||||||
key = "anthropic-api-key"
|
key = "ANTHROPIC_API_KEY"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,8 +68,8 @@ resource "kubernetes_deployment_v1" "orkester" {
|
|||||||
|
|
||||||
value_from {
|
value_from {
|
||||||
secret_key_ref {
|
secret_key_ref {
|
||||||
name = kubernetes_secret_v1.hemligheter.metadata[0].name
|
name = "felsokning-hemligheter"
|
||||||
key = "jwt-secret"
|
key = "JWT_SECRET"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# Trafiken utifrån och in. En domän, fyra prefix — se local.routing.
|
# Trafiken utifrån och in.
|
||||||
|
#
|
||||||
|
# En ALB, skapad av lastbalanserar-kontrollern ur det här objektet.
|
||||||
|
# Certifikatet kommer från ACM och terminerar på lastbalanseraren; all
|
||||||
|
# trafik mot :80 skickas vidare till :443.
|
||||||
#
|
#
|
||||||
# Kroppsgränsen är inte kosmetisk: teknikern dokumenterar med foto och
|
# Kroppsgränsen är inte kosmetisk: teknikern dokumenterar med foto och
|
||||||
# video som följer med händelseloggen. Ingressens standardgräns (1 MB)
|
# video. En för snäv gräns avvisar dokumentationen redan vid dörren.
|
||||||
# skulle avvisa dokumentationen redan vid dörren, medan tjänsten själv
|
|
||||||
# accepterar 4 MB. Gränsen sätts därför uttryckligen och med marginal.
|
|
||||||
|
|
||||||
resource "kubernetes_ingress_v1" "denna" {
|
resource "kubernetes_ingress_v1" "denna" {
|
||||||
metadata {
|
metadata {
|
||||||
@@ -12,23 +14,33 @@ resource "kubernetes_ingress_v1" "denna" {
|
|||||||
labels = local.etiketter
|
labels = local.etiketter
|
||||||
|
|
||||||
annotations = {
|
annotations = {
|
||||||
"cert-manager.io/cluster-issuer" = var.cert_issuer
|
"alb.ingress.kubernetes.io/scheme" = "internet-facing"
|
||||||
"nginx.ingress.kubernetes.io/proxy-body-size" = "${var.max_kropp_mb}m"
|
"alb.ingress.kubernetes.io/target-type" = "ip"
|
||||||
"nginx.ingress.kubernetes.io/proxy-read-timeout" = "120" # AI-svar kan ta tid
|
"alb.ingress.kubernetes.io/certificate-arn" = local.aws.certifikat_arn
|
||||||
"nginx.ingress.kubernetes.io/ssl-redirect" = "true"
|
"alb.ingress.kubernetes.io/listen-ports" = jsonencode([{ HTTP = 80 }, { HTTPS = 443 }])
|
||||||
|
"alb.ingress.kubernetes.io/ssl-redirect" = "443"
|
||||||
|
"alb.ingress.kubernetes.io/ssl-policy" = "ELBSecurityPolicy-TLS13-1-2-2021-06"
|
||||||
|
|
||||||
|
# Hälsokontrollen går mot plattformen — den vet om databasen svarar.
|
||||||
|
"alb.ingress.kubernetes.io/healthcheck-path" = "/halsa"
|
||||||
|
|
||||||
|
# AI-svar kan ta tid; standardgränsen på 60 s räcker inte.
|
||||||
|
"alb.ingress.kubernetes.io/load-balancer-attributes" = join(",", [
|
||||||
|
"idle_timeout.timeout_seconds=180",
|
||||||
|
"routing.http.drop_invalid_header_fields.enabled=true",
|
||||||
|
"access_logs.s3.enabled=false",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Alla tre tjänsterna delar en lastbalanserare.
|
||||||
|
"alb.ingress.kubernetes.io/group.name" = local.namn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
spec {
|
spec {
|
||||||
ingress_class_name = var.ingress_klass
|
ingress_class_name = "alb"
|
||||||
|
|
||||||
tls {
|
|
||||||
hosts = [var.doman]
|
|
||||||
secret_name = "${local.namn}-tls"
|
|
||||||
}
|
|
||||||
|
|
||||||
rule {
|
rule {
|
||||||
host = var.doman
|
host = local.aws.doman
|
||||||
|
|
||||||
http {
|
http {
|
||||||
dynamic "path" {
|
dynamic "path" {
|
||||||
@@ -54,8 +66,33 @@ resource "kubernetes_ingress_v1" "denna" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
depends_on = [
|
depends_on = [
|
||||||
|
helm_release.lastbalanserare,
|
||||||
kubernetes_service_v1.web,
|
kubernetes_service_v1.web,
|
||||||
kubernetes_service_v1.plattform,
|
kubernetes_service_v1.plattform,
|
||||||
kubernetes_service_v1.orkester,
|
kubernetes_service_v1.orkester,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# DNS pekar på lastbalanseraren. Alias i stället för CNAME så att
|
||||||
|
# domänens rot kan användas om ni vill det senare.
|
||||||
|
resource "aws_route53_record" "denna" {
|
||||||
|
zone_id = local.aws.zon_id
|
||||||
|
name = local.aws.doman
|
||||||
|
type = "A"
|
||||||
|
|
||||||
|
alias {
|
||||||
|
name = kubernetes_ingress_v1.denna.status[0].load_balancer[0].ingress[0].hostname
|
||||||
|
zone_id = data.aws_lb.denna.zone_id
|
||||||
|
evaluate_target_health = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "aws_lb" "denna" {
|
||||||
|
tags = {
|
||||||
|
"elbv2.k8s.aws/cluster" = local.aws.kluster
|
||||||
|
"ingress.k8s.aws/stack" = local.namn
|
||||||
|
"ingress.k8s.aws/resource" = "LoadBalancer"
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [kubernetes_ingress_v1.denna]
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,40 +22,6 @@ resource "kubernetes_network_policy_v1" "neka_allt_in" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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.
|
# De tre webbtjänsterna: bara från ingress-kontrollern.
|
||||||
resource "kubernetes_network_policy_v1" "tjanster_in" {
|
resource "kubernetes_network_policy_v1" "tjanster_in" {
|
||||||
for_each = toset(["web", "plattform", "ai-orkester"])
|
for_each = toset(["web", "plattform", "ai-orkester"])
|
||||||
@@ -75,10 +41,10 @@ resource "kubernetes_network_policy_v1" "tjanster_in" {
|
|||||||
|
|
||||||
ingress {
|
ingress {
|
||||||
from {
|
from {
|
||||||
namespace_selector {
|
# ALB når podarna direkt på deras IP (target-type: ip), så
|
||||||
match_labels = {
|
# trafiken kommer från VPC:ns adressrymd och inte från en pod.
|
||||||
"kubernetes.io/metadata.name" = var.ingress_namnrymd
|
ip_block {
|
||||||
}
|
cidr = "0.0.0.0/0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,25 +100,6 @@ 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
|
|
||||||
labels = local.etiketter
|
|
||||||
}
|
|
||||||
|
|
||||||
spec {
|
|
||||||
pod_selector {
|
|
||||||
match_labels = { app = "postgres" }
|
|
||||||
}
|
|
||||||
|
|
||||||
policy_types = ["Egress"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Plattformen: databasen internt, och HTTPS ut till kundernas
|
# Plattformen: databasen internt, och HTTPS ut till kundernas
|
||||||
# leverantörer. Klustrets och molnets interna adresser är undantagna —
|
# 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
|
# samma gräns som koden själv upprätthåller (pekarInat), här en gång till
|
||||||
@@ -171,23 +118,11 @@ resource "kubernetes_network_policy_v1" "plattform_ut" {
|
|||||||
|
|
||||||
policy_types = ["Egress"]
|
policy_types = ["Egress"]
|
||||||
|
|
||||||
# Databasen i klustret — inbyggt eller CloudNativePG. I externt läge
|
# Aurora ligger utanför klustret, i ett eget subnätlager.
|
||||||
# finns ingen databaspod att peka på; anslutningen går ut som vanlig
|
egress {
|
||||||
# utgående trafik nedan.
|
ports {
|
||||||
dynamic "egress" {
|
port = "5432"
|
||||||
for_each = local.extern_databas ? [] : [1]
|
protocol = "TCP"
|
||||||
|
|
||||||
content {
|
|
||||||
to {
|
|
||||||
pod_selector {
|
|
||||||
match_labels = local.inbyggd_databas ? { app = "postgres" } : { "cnpg.io/cluster" = "felsokning-db" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ports {
|
|
||||||
port = "5432"
|
|
||||||
protocol = "TCP"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,17 +148,6 @@ resource "kubernetes_network_policy_v1" "plattform_ut" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Managerad databas utanför klustret: 5432 mot leverantören.
|
|
||||||
dynamic "egress" {
|
|
||||||
for_each = local.extern_databas ? [1] : []
|
|
||||||
|
|
||||||
content {
|
|
||||||
ports {
|
|
||||||
port = "5432"
|
|
||||||
protocol = "TCP"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# Självhostad git med egna byggrunners.
|
||||||
|
#
|
||||||
|
# Med den här på finns inget externt beroende kvar i kedjan: källkoden,
|
||||||
|
# bygget, registret och driftsättningen ligger allt i vår egen
|
||||||
|
# AWS-miljö.
|
||||||
|
#
|
||||||
|
# Gitea Actions använder samma workflow-syntax som GitHub Actions, så
|
||||||
|
# .gitea/workflows/felsokning.yml är samma fil som tidigare låg under
|
||||||
|
# .github — bara flyttad och pekad på egna runners.
|
||||||
|
#
|
||||||
|
# Runnern kör byggen. Den har därför sitt eget tjänstekonto med en roll
|
||||||
|
# som bara får publicera till ECR, och ett separat konto för
|
||||||
|
# driftsättning som bara får röra klustret. Ett komprometterat bygge kan
|
||||||
|
# inte driftsätta.
|
||||||
|
|
||||||
|
locals {
|
||||||
|
gitea_pa = var.gitea.aktiv
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_namespace_v1" "gitea" {
|
||||||
|
count = local.gitea_pa ? 1 : 0
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
name = "gitea"
|
||||||
|
labels = local.etiketter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Byggkontot. Rollen skapades i AWS-basen och är bunden till exakt
|
||||||
|
# gitea:runner — inget annat tjänstekonto kan anta den.
|
||||||
|
resource "kubernetes_service_account_v1" "runner" {
|
||||||
|
count = local.gitea_pa ? 1 : 0
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
name = "runner"
|
||||||
|
namespace = kubernetes_namespace_v1.gitea[0].metadata[0].name
|
||||||
|
labels = local.etiketter
|
||||||
|
|
||||||
|
annotations = {
|
||||||
|
"eks.amazonaws.com/role-arn" = data.terraform_remote_state.bas.outputs.karta.roller.bygg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service_account_v1" "drift" {
|
||||||
|
count = local.gitea_pa ? 1 : 0
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
name = "drift"
|
||||||
|
namespace = kubernetes_namespace_v1.gitea[0].metadata[0].name
|
||||||
|
labels = local.etiketter
|
||||||
|
|
||||||
|
annotations = {
|
||||||
|
"eks.amazonaws.com/role-arn" = data.terraform_remote_state.bas.outputs.karta.roller.drift
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Registreringstoken för runnern. Genereras här och delas mellan Gitea
|
||||||
|
# och runnern — de behöver komma överens utan att någon skriver in den
|
||||||
|
# för hand.
|
||||||
|
resource "random_password" "runner_token" {
|
||||||
|
count = local.gitea_pa ? 1 : 0
|
||||||
|
|
||||||
|
length = 40
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_secret_v1" "runner" {
|
||||||
|
count = local.gitea_pa ? 1 : 0
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
name = "runner-registrering"
|
||||||
|
namespace = kubernetes_namespace_v1.gitea[0].metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
token = random_password.runner_token[0].result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "helm_release" "gitea" {
|
||||||
|
count = local.gitea_pa ? 1 : 0
|
||||||
|
|
||||||
|
name = "gitea"
|
||||||
|
repository = "https://dl.gitea.com/charts/"
|
||||||
|
chart = "gitea"
|
||||||
|
version = "10.6.0"
|
||||||
|
namespace = kubernetes_namespace_v1.gitea[0].metadata[0].name
|
||||||
|
|
||||||
|
# Gitea kan använda en extern databas. Vi kör den inbyggda SQLite på en
|
||||||
|
# volym i stället: en verkstadsinstallation har en handfull utvecklare,
|
||||||
|
# och en extra Aurora-databas att säkerhetskopiera och övervaka är
|
||||||
|
# kostnad utan motsvarande nytta. Volymen ligger på EBS med
|
||||||
|
# ögonblicksbilder.
|
||||||
|
values = [yamlencode({
|
||||||
|
redis-cluster = { enabled = false }
|
||||||
|
postgresql-ha = { enabled = false }
|
||||||
|
|
||||||
|
persistence = {
|
||||||
|
enabled = true
|
||||||
|
size = var.gitea.lagring
|
||||||
|
storageClass = "gp2"
|
||||||
|
}
|
||||||
|
|
||||||
|
gitea = {
|
||||||
|
config = {
|
||||||
|
server = {
|
||||||
|
DOMAIN = var.gitea.doman
|
||||||
|
ROOT_URL = "https://${var.gitea.doman}/"
|
||||||
|
SSH_DOMAIN = var.gitea.doman
|
||||||
|
}
|
||||||
|
database = { DB_TYPE = "sqlite3" }
|
||||||
|
# Ingen självregistrering: konton skapas av administratören.
|
||||||
|
service = {
|
||||||
|
DISABLE_REGISTRATION = true
|
||||||
|
REQUIRE_SIGNIN_VIEW = true
|
||||||
|
}
|
||||||
|
actions = { ENABLED = true }
|
||||||
|
# Bygg-loggarna hamnar i CloudWatch via containerloggarna.
|
||||||
|
log = { LEVEL = "Info" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runnern kör i samma namnrymd med byggkontot.
|
||||||
|
actions = {
|
||||||
|
enabled = true
|
||||||
|
statefulset = {
|
||||||
|
replicas = var.gitea.runners
|
||||||
|
serviceAccountName = kubernetes_service_account_v1.runner[0].metadata[0].name
|
||||||
|
}
|
||||||
|
existingSecret = kubernetes_secret_v1.runner[0].metadata[0].name
|
||||||
|
existingSecretKey = "token"
|
||||||
|
}
|
||||||
|
})]
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
helm_release.external_secrets,
|
||||||
|
kubernetes_secret_v1.runner,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gitea får sin egen väg in genom samma lastbalanserare — grupp-namnet
|
||||||
|
# är detsamma, så ALB:n delas i stället för att en till skapas.
|
||||||
|
resource "kubernetes_ingress_v1" "gitea" {
|
||||||
|
count = local.gitea_pa && var.gitea.doman != "" ? 1 : 0
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
name = "gitea"
|
||||||
|
namespace = kubernetes_namespace_v1.gitea[0].metadata[0].name
|
||||||
|
labels = local.etiketter
|
||||||
|
|
||||||
|
annotations = {
|
||||||
|
"alb.ingress.kubernetes.io/scheme" = "internet-facing"
|
||||||
|
"alb.ingress.kubernetes.io/target-type" = "ip"
|
||||||
|
"alb.ingress.kubernetes.io/certificate-arn" = local.aws.certifikat_arn
|
||||||
|
"alb.ingress.kubernetes.io/listen-ports" = jsonencode([{ HTTP = 80 }, { HTTPS = 443 }])
|
||||||
|
"alb.ingress.kubernetes.io/ssl-redirect" = "443"
|
||||||
|
"alb.ingress.kubernetes.io/group.name" = local.namn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
ingress_class_name = "alb"
|
||||||
|
|
||||||
|
rule {
|
||||||
|
host = var.gitea.doman
|
||||||
|
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
path_type = "Prefix"
|
||||||
|
|
||||||
|
backend {
|
||||||
|
service {
|
||||||
|
name = "gitea-http"
|
||||||
|
|
||||||
|
port {
|
||||||
|
number = 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.gitea]
|
||||||
|
}
|
||||||
@@ -8,12 +8,14 @@ begripliga att läsa:
|
|||||||
| `versions.tf` | Leverantörer och tillståndsbackend |
|
| `versions.tf` | Leverantörer och tillståndsbackend |
|
||||||
| `variables.tf` | Allt som skiljer en installation från en annan |
|
| `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 |
|
| `karta.tf` | **Systemet som data** — tjänster, portar, routing, hemligheter, dataflöden, gränser |
|
||||||
| `10-namnrymd.tf` | Namnrymd, hemligheten, databasschemat |
|
| `10-namnrymd.tf` | Namnrymd, tjänstekonto med IRSA, hemligheter ur Secrets Manager |
|
||||||
| `20-databas.tf` | Händelseloggen — tre lägen, se nedan |
|
| `05-aws.tf` | Kopplingen till AWS-basen — allt den redan bestämt läses här |
|
||||||
|
| `15-plattformstjanster.tf` | ALB-kontroller, External Secrets, metrics, CloudWatch |
|
||||||
|
| `90-gitea.tf` | Självhostad git med egna byggrunners |
|
||||||
| `30-plattform.tf` | Backend: auth, händelse-API, delning, integrationer |
|
| `30-plattform.tf` | Backend: auth, händelse-API, delning, integrationer |
|
||||||
| `40-orkester.tf` | AI-orkestern |
|
| `40-orkester.tf` | AI-orkestern |
|
||||||
| `50-web.tf` | Klienten |
|
| `50-web.tf` | Klienten |
|
||||||
| `60-ingress.tf` | Trafiken utifrån |
|
| `60-ingress.tf` | ALB och DNS |
|
||||||
| `70-natverk.tf` | Nätverkspolicyer — vem får prata med vem |
|
| `70-natverk.tf` | Nätverkspolicyer — vem får prata med vem |
|
||||||
| `outputs.tf` | Kartan utskriven |
|
| `outputs.tf` | Kartan utskriven |
|
||||||
|
|
||||||
@@ -45,33 +47,24 @@ terraform validate # typer och referenser
|
|||||||
`terraform validate` kräver att leverantörerna hämtats från
|
`terraform validate` kräver att leverantörerna hämtats från
|
||||||
registry.terraform.io.
|
registry.terraform.io.
|
||||||
|
|
||||||
## Databasen: tre lägen
|
## Var saker ligger
|
||||||
|
|
||||||
`var.databas_lage` saknar standardvärde med flit. Valet avgör om det
|
Det här lagret bestämmer nästan ingenting själv. AWS-basen
|
||||||
finns säkerhetskopiering, och det ska inte kunna bli fel av slentrian.
|
(`felsokning/infra/aws`) äger klustret, databasen, registret, hinken,
|
||||||
|
rollerna, domänen och hemligheterna — och `05-aws.tf` läser dess utdata.
|
||||||
|
Ändras något där slår det igenom här utan att en rad ändras.
|
||||||
|
|
||||||
| Läge | Backup | Failover | Använd när |
|
| Vad | Var |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- |
|
||||||
| `extern` | Leverantörens, med PITR | Leverantörens | **Produktion.** Cloud SQL, RDS, Neon, Azure Flexible Server |
|
| Händelseloggen | Aurora PostgreSQL utanför klustret, med PITR |
|
||||||
| `cnpg` | Basbackup 02:30 + WAL-arkiv → objektlagring, PITR | Ja, `databas_instanser` styr | Produktion när databasen måste ligga i klustret |
|
| Bilagor | S3, signerat med tjänstekontots roll — inga nycklar finns |
|
||||||
| `inbyggd` | **Ingen** | Nej | Prov och demo. Blockeras av en precondition när `miljo = "produktion"` |
|
| Hemligheter | Secrets Manager, speglade av External Secrets var timme |
|
||||||
|
| Bilder | ECR med oföränderliga taggar |
|
||||||
|
| Trafik in | ALB med ACM-certifikat |
|
||||||
|
| Git och bygge | Gitea med egna runners i samma kluster |
|
||||||
|
|
||||||
Går händelseloggen förlorad är det inte "data" som försvinner utan varje
|
Terraform ser aldrig en hemlighets värde. Det är avsiktligt: en
|
||||||
ärendes bevisvärde: vad som kontrollerades, av vem, när, med vilken
|
hemlighet som passerar Terraform hamnar i tillståndsfilen.
|
||||||
evidens. Det går inte att återskapa i efterhand.
|
|
||||||
|
|
||||||
`cnpg` kräver att CloudNativePG-operatorn redan är installerad —
|
|
||||||
Terraform slår upp dess CRD vid plan:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl apply --server-side -f \
|
|
||||||
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.25/releases/cnpg-1.25.0.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## Driftsättning
|
||||||
|
|
||||||
|
|||||||
@@ -29,38 +29,27 @@ locals {
|
|||||||
tjanster = {
|
tjanster = {
|
||||||
web = {
|
web = {
|
||||||
roll = "Klienten. Statisk SPA bakom oprivilegierad nginx. Innehåller ingen hemlighet — API-adressen bakas in vid bygget."
|
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}"
|
bild = "${local.aws.register}/${local.namn}-web:${var.bildtagg}"
|
||||||
hemligt = []
|
hemligt = []
|
||||||
utat = ["webbläsaren anropar plattform och orkester direkt över ingressen"]
|
utat = ["webbläsaren anropar plattform och orkester direkt över ingressen"]
|
||||||
}
|
}
|
||||||
plattform = {
|
plattform = {
|
||||||
roll = "Backend. Auth, append-only händelse-API, Live Share, organisationsinställningar, ECM-regelpaket, märkesspecifika kopplingar."
|
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}"
|
bild = "${local.aws.register}/${local.namn}-plattform:${var.bildtagg}"
|
||||||
hemligt = concat(
|
hemligt = ["allt via Secrets Manager: databas, Claude-nyckel, JWT, krypteringsnyckel"]
|
||||||
["jwt-hemlighet", "postgres-losenord", "integration-nyckel"],
|
utat = ["Aurora:5432", "S3 för bilagor", "kundernas leverantörer (spärrat mot privata nät)"]
|
||||||
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 = {
|
orkester = {
|
||||||
roll = "AI-orkestern. Routar per uppgift till Claude, äger systemprompt och svarsschema. Verifierar plattformens JWT."
|
roll = "AI-orkestern. Routar per uppgift till Claude, äger systemprompt och svarsschema. Verifierar plattformens JWT."
|
||||||
bild = "${var.register}/${local.namn}-ai-orkester:${var.bildtagg}"
|
bild = "${local.aws.register}/${local.namn}-ai-orkester:${var.bildtagg}"
|
||||||
hemligt = ["anthropic-api-nyckel", "jwt-hemlighet"]
|
hemligt = ["anthropic-api-nyckel", "jwt-hemlighet"]
|
||||||
utat = ["api.anthropic.com"]
|
utat = ["api.anthropic.com"]
|
||||||
}
|
}
|
||||||
postgres = {
|
postgres = {
|
||||||
roll = join(" ", [
|
roll = "Händelseloggen ligger i Aurora utanför klustret — se AWS-basen. Append-only garanteras av databastriggers, säkerhetskopiering och PITR av Aurora."
|
||||||
"Händelseloggen — systemets enda sanningskälla. Append-only garanteras av databastriggers, inte bara av API:t.",
|
bild = "(Aurora PostgreSQL, managerad)"
|
||||||
var.databas_lage == "extern" ? "Läge: extern managerad Postgres — leverantören sköter backup och PITR." :
|
hemligt = ["databasens anslutningssträng, ur Secrets Manager"]
|
||||||
var.databas_lage == "cnpg" ? "Läge: CloudNativePG i klustret — basbackup, WAL-arkivering, PITR och failover." :
|
utat = []
|
||||||
"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"] : []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,15 +70,9 @@ locals {
|
|||||||
# En enda Secret, men varje tjänst monterar bara sina egna nycklar.
|
# En enda Secret, men varje tjänst monterar bara sina egna nycklar.
|
||||||
|
|
||||||
hemligheter = {
|
hemligheter = {
|
||||||
"anthropic-api-key" = var.anthropic_api_nyckel
|
kalla = "AWS Secrets Manager — Terraform ser aldrig värdena"
|
||||||
"jwt-secret" = coalesce(var.jwt_hemlighet, random_password.jwt.result)
|
speglas = "External Secrets, var timme, med tjänstekontots roll"
|
||||||
"postgres-losenord" = coalesce(var.postgres_losenord, random_password.postgres.result)
|
lasare = "endast plattformens tjänstekonto, via IRSA"
|
||||||
"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 -----------------------------------------------------
|
# ---- Dataflöden -----------------------------------------------------
|
||||||
@@ -105,12 +88,13 @@ locals {
|
|||||||
]
|
]
|
||||||
|
|
||||||
bilagor = {
|
bilagor = {
|
||||||
lage = var.bilage_lage
|
lage = "s3"
|
||||||
var = var.bilage_lage == "s3" ? "${var.s3.endpoint}/${var.s3.hink}/${var.s3.prefix}" : "tabellen bilage_innehall"
|
var = "S3-hinken ur AWS-basen"
|
||||||
hur = join(" ", [
|
hur = join(" ", [
|
||||||
"Innehållet ligger utanför händelsen; loggen bär referensen och innehållets SHA-256.",
|
"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.",
|
"Innehållsadresserat, så samma foto lagras en gång.",
|
||||||
"Hashen kontrolleras vid utlämning — en utbytt bild lämnas inte ut.",
|
"Hashen kontrolleras vid utlämning — en utbytt bild lämnas inte ut.",
|
||||||
|
"Tjänsten signerar mot S3 med tjänstekontots roll; inga nycklar finns.",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,32 +103,15 @@ locals {
|
|||||||
"Delningsgränsen: tillåtelselista över händelsetyper per nivå (kund/partner/intern) — nya typer är interna tills de aktivt släpps fram.",
|
"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.",
|
"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.",
|
"Historikgränsen: append-only i både API och databas (triggers). Ingen roll kan ändra eller radera en händelse.",
|
||||||
|
"AWS-gränsen: varje roll är bunden till exakt ett tjänstekonto i en namnrymd (IRSA). Bygget får publicera men inte driftsätta; driften tvärtom.",
|
||||||
"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.",
|
"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 ------------------------------------
|
# ---- Det som medvetet inte ingår ------------------------------------
|
||||||
|
|
||||||
avgransningar = concat(
|
avgransningar = [
|
||||||
var.databas_lage == "inbyggd" ? ["INGEN SÄKERHETSKOPIERING — läget inbyggd har en volym och inget mer. Endast prov och demo."] : [],
|
"Observability är CloudWatch: loggar, Container Insights och fyra larm. Ingen distribuerad tracing.",
|
||||||
[
|
"Takt-begränsning på inloggning är databasbackad och håller bakom flera repliker; övriga endpoints har ingen.",
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ output "karta" {
|
|||||||
|
|
||||||
value = {
|
value = {
|
||||||
miljo = var.miljo
|
miljo = var.miljo
|
||||||
domän = "https://${var.doman}"
|
domän = "https://${local.aws.doman}"
|
||||||
namnrymd = var.namnrymd
|
namnrymd = var.namnrymd
|
||||||
bildtagg = var.bildtagg
|
bildtagg = var.bildtagg
|
||||||
|
|
||||||
@@ -22,31 +22,17 @@ output "karta" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
routing = [
|
routing = [
|
||||||
for r in local.routing : "https://${var.doman}${r.prefix} → ${r.till} (${r.varfor})"
|
for r in local.routing : "https://${local.aws.doman}${r.prefix} → ${r.till} (${r.varfor})"
|
||||||
]
|
]
|
||||||
|
|
||||||
dataflöden = local.dataflode
|
dataflöden = local.dataflode
|
||||||
gränser = local.granser
|
gränser = local.granser
|
||||||
|
|
||||||
databas = {
|
databas = {
|
||||||
läge = var.databas_lage
|
motor = "Aurora PostgreSQL Serverless v2 — utanför klustret, se AWS-basen"
|
||||||
motor = "PostgreSQL 17"
|
säkerhet = "append-only via triggers; ingen roll kan ändra eller radera en händelse"
|
||||||
säkerhet = "append-only via triggers — historik kan inte ändras eller raderas av någon roll"
|
backup = "Auroras automatiska säkerhetskopiering med PITR"
|
||||||
|
atkomst = "endast från klustrets noder, i ett subnätlager utan routing ut"
|
||||||
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)"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bilagor = local.bilagor
|
bilagor = local.bilagor
|
||||||
@@ -54,8 +40,9 @@ output "karta" {
|
|||||||
drift = {
|
drift = {
|
||||||
registrering_öppen = var.registrering_oppen
|
registrering_öppen = var.registrering_oppen
|
||||||
interna_uppslag_tillåtna = var.tillat_interna_uppslag
|
interna_uppslag_tillåtna = var.tillat_interna_uppslag
|
||||||
max_kropp = "${var.max_kropp_mb} MB (ingress) / 4 MB (tjänst)"
|
max_kropp = "${var.max_kropp_mb} MB (lastbalanserare) / 32 MB (bilaga) / 4 MB (händelse)"
|
||||||
cors = "https://${var.doman}"
|
cors = "https://${local.aws.doman}"
|
||||||
|
hemligheter = "AWS Secrets Manager, speglade av External Secrets"
|
||||||
}
|
}
|
||||||
|
|
||||||
avgränsningar = local.avgransningar
|
avgränsningar = local.avgransningar
|
||||||
@@ -66,21 +53,12 @@ output "endpoints" {
|
|||||||
description = "Adresser att kontrollera efter driftsättning."
|
description = "Adresser att kontrollera efter driftsättning."
|
||||||
|
|
||||||
value = {
|
value = {
|
||||||
klient = "https://${var.doman}/felsokning"
|
klient = "https://${local.aws.doman}/felsokning"
|
||||||
hälsa = "https://${var.doman}/halsa"
|
hälsa = "https://${local.aws.doman}/halsa"
|
||||||
api_spec = "https://${var.doman}/api/openapi.yaml"
|
api_spec = "https://${local.aws.doman}/api/openapi.yaml"
|
||||||
ai = "https://${var.doman}/api/ai"
|
ai = "https://${local.aws.doman}/api/ai"
|
||||||
delningslänk = "https://${var.doman}/felsokning/delad/<kod>"
|
delningslänk = "https://${local.aws.doman}/felsokning/delad/<kod>"
|
||||||
|
git = var.gitea.aktiv && var.gitea.doman != "" ? "https://${var.gitea.doman}" : "(gitea av)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output "genererade_hemligheter" {
|
|
||||||
description = "Hemligheter Terraform genererat (tomma om ni satt egna). Finns i tillståndet — skydda det."
|
|
||||||
sensitive = true
|
|
||||||
|
|
||||||
value = {
|
|
||||||
jwt_hemlighet = var.jwt_hemlighet == "" ? random_password.jwt.result : "(egen)"
|
|
||||||
postgres_losenord = var.postgres_losenord == "" ? random_password.postgres.result : "(egen)"
|
|
||||||
integration_nyckel = var.integration_nyckel == "" ? random_id.integration_nyckel.hex : "(egen)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,61 +1,28 @@
|
|||||||
# Kopiera till terraform.tfvars och fyll i. Checka aldrig in riktiga
|
# Kopiera till terraform.tfvars och fyll i. Nästan allt kommer från
|
||||||
# nycklar — .gitignore utesluter *.tfvars.
|
# AWS-basen — det här är resten.
|
||||||
|
|
||||||
doman = "app.exempel.se"
|
# Hinken där basens tillstånd ligger. Basen skriver ut namnet:
|
||||||
register = "ghcr.io/min-organisation"
|
# cd ../aws && terraform output -raw ...
|
||||||
bildtagg = "ersatt-med-git-sha"
|
tillstandshink = "felsokning-produktion-tfstate-123456789012"
|
||||||
|
region = "eu-north-1"
|
||||||
|
|
||||||
|
# Commit-SHA från publiceringsflödet. ECR har oföränderliga taggar, så
|
||||||
|
# en tagg pekar alltid på samma bygge.
|
||||||
|
bildtagg = "ersatt-med-commit-sha"
|
||||||
|
|
||||||
miljo = "produktion"
|
miljo = "produktion"
|
||||||
|
|
||||||
# Claude-nyckeln ägs av plattformen och når aldrig klienten.
|
# Stäng självregistrering i produktion.
|
||||||
# Hämta hellre från en secrets-hanterare än att skriva den här:
|
|
||||||
# export TF_VAR_anthropic_api_nyckel="$(vault kv get -field=key ...)"
|
|
||||||
anthropic_api_nyckel = "sk-ant-…"
|
|
||||||
|
|
||||||
# Lämnas dessa tomma genererar Terraform dem vid första körningen och
|
|
||||||
# behåller dem i tillståndet. Byts integration_nyckel måste alla
|
|
||||||
# märkesspecifika kopplingar sparas om.
|
|
||||||
jwt_hemlighet = ""
|
|
||||||
postgres_losenord = ""
|
|
||||||
integration_nyckel = ""
|
|
||||||
|
|
||||||
# Stäng självregistrering i produktion — annars kan vem som helst skapa
|
|
||||||
# en organisation.
|
|
||||||
registrering_oppen = false
|
registrering_oppen = false
|
||||||
|
|
||||||
# Öppnar en väg från en tenant-administratör in i klustrets interna
|
# Öppnar en väg från en tenant-administratör in i klustrets interna
|
||||||
# adresser. Sätt bara true om verkstaden har en OEM-server på eget nät.
|
# adresser. Sätt bara true om verkstaden har en OEM-server på eget nät.
|
||||||
tillat_interna_uppslag = false
|
tillat_interna_uppslag = false
|
||||||
|
|
||||||
# --- Databasen ---------------------------------------------------------
|
# Självhostad git med egna runners.
|
||||||
#
|
gitea = {
|
||||||
# Inget standardvärde: valet avgör om det finns säkerhetskopiering.
|
aktiv = true
|
||||||
#
|
doman = "git.exempel.se"
|
||||||
# "extern" managerad Postgres — leverantören sköter backup och PITR
|
runners = 2
|
||||||
# (rekommenderat i produktion). Kräver databas_url.
|
lagring = "20Gi"
|
||||||
# "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"
|
|
||||||
|
|
||||||
repliker = {
|
|
||||||
web = 2
|
|
||||||
plattform = 2
|
|
||||||
orkester = 2
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,45 @@
|
|||||||
# Allt som skiljer en installation från en annan. Inget annat ska
|
# Det lilla som inte kommer från AWS-basen.
|
||||||
# behöva ändras i .tf-filerna för att driftsätta i en ny miljö.
|
#
|
||||||
|
# Domän, register, roller, certifikat, hink och hemligheternas namn läses
|
||||||
|
# ur basens utdata (05-aws.tf) — de ska inte anges två gånger.
|
||||||
|
|
||||||
# ---- Kluster -----------------------------------------------------------
|
variable "tillstandshink" {
|
||||||
|
description = "S3-hinken där AWS-basens Terraform-tillstånd ligger. Basen skriver ut namnet."
|
||||||
variable "kubeconfig" {
|
|
||||||
description = "Sökväg till kubeconfig."
|
|
||||||
type = string
|
type = string
|
||||||
default = "~/.kube/config"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "kube_context" {
|
variable "region" {
|
||||||
description = "Kubernetes-kontext att driftsätta i. Tom = kubeconfigens aktuella."
|
description = "Region för tillståndshinken. Samma som basens."
|
||||||
type = string
|
type = string
|
||||||
default = ""
|
default = "eu-north-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "namnrymd" {
|
variable "namnrymd" {
|
||||||
description = "Namnrymd för hela systemet."
|
description = "Namnrymd för arbetslasten."
|
||||||
type = string
|
type = string
|
||||||
default = "guidad-felsokning"
|
default = "guidad-felsokning"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "miljo" {
|
variable "miljo" {
|
||||||
description = "Miljöns namn (produktion, test, demo). Sätts som label på allt."
|
description = "Miljöns namn. Sätts som label på allt."
|
||||||
type = string
|
type = string
|
||||||
default = "produktion"
|
default = "produktion"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Domän och certifikat ---------------------------------------------
|
|
||||||
|
|
||||||
variable "doman" {
|
|
||||||
description = "Domänen klienten och API:t nås på, t.ex. app.exempel.se."
|
|
||||||
type = string
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "cert_issuer" {
|
|
||||||
description = "cert-manager ClusterIssuer som utfärdar TLS-certifikatet."
|
|
||||||
type = string
|
|
||||||
default = "letsencrypt"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "ingress_klass" {
|
|
||||||
description = "IngressClass. Nätverkspolicyn öppnar för ingress-kontrollerns namnrymd nedan."
|
|
||||||
type = string
|
|
||||||
default = "nginx"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "ingress_namnrymd" {
|
|
||||||
description = "Namnrymd där ingress-kontrollern kör — behövs av nätverkspolicyn."
|
|
||||||
type = string
|
|
||||||
default = "ingress-nginx"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---- Bilder ------------------------------------------------------------
|
|
||||||
|
|
||||||
variable "register" {
|
|
||||||
description = "Containerregister, t.ex. ghcr.io/min-organisation."
|
|
||||||
type = string
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "bildtagg" {
|
variable "bildtagg" {
|
||||||
description = <<-TEXT
|
description = <<-TEXT
|
||||||
Taggen alla tre bilderna körs med — normalt git-SHA:t från
|
Taggen alla tre bilderna körs med — commit-SHA:t från
|
||||||
publiceringsflödet. "latest" duger för prov men gör en rullning
|
publiceringsflödet. ECR har oföränderliga taggar, så en tagg pekar
|
||||||
omöjlig att spåra.
|
alltid på samma bygge.
|
||||||
TEXT
|
TEXT
|
||||||
type = string
|
type = string
|
||||||
|
|
||||||
validation {
|
validation {
|
||||||
condition = var.bildtagg != ""
|
condition = var.bildtagg != ""
|
||||||
error_message = "Ange en bildtagg — helst git-SHA:t, aldrig tomt."
|
error_message = "Ange en bildtagg — helst commit-SHA:t, aldrig tomt."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Hemligheter -------------------------------------------------------
|
|
||||||
#
|
|
||||||
# Sätts helst via en secrets-hanterare (External Secrets, Vault, molnets
|
|
||||||
# secret manager) i stället för tfvars. Lämnas de tomma genererar
|
|
||||||
# Terraform slumpvärden vid första körningen och behåller dem i
|
|
||||||
# tillståndet — bekvämt för prov, men då måste tillståndet skyddas
|
|
||||||
# därefter.
|
|
||||||
|
|
||||||
variable "anthropic_api_nyckel" {
|
|
||||||
description = "Claude API-nyckel. Ägs av plattformen, når aldrig klienten."
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "jwt_hemlighet" {
|
|
||||||
description = "HS256-hemlighet. Delas av plattformen och orkestern. Tom = genereras."
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "postgres_losenord" {
|
|
||||||
description = "Databaslösenord. Tom = genereras."
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "integration_nyckel" {
|
|
||||||
description = <<-TEXT
|
|
||||||
32 byte hex som krypterar kundernas leverantörsuppgifter i vila.
|
|
||||||
Tom = genereras. Byts nyckeln måste alla kopplingar sparas om —
|
|
||||||
tjänsten visar då inga värden i stället för att gissa.
|
|
||||||
TEXT
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---- Drift -------------------------------------------------------------
|
# ---- Drift -------------------------------------------------------------
|
||||||
|
|
||||||
variable "registrering_oppen" {
|
variable "registrering_oppen" {
|
||||||
@@ -133,12 +61,12 @@ variable "tillat_interna_uppslag" {
|
|||||||
|
|
||||||
variable "max_kropp_mb" {
|
variable "max_kropp_mb" {
|
||||||
description = <<-TEXT
|
description = <<-TEXT
|
||||||
Största tillåtna anropskropp. Foton och videoklipp följer med i
|
Största tillåtna anropskropp. Bilagor laddas upp separat och kan
|
||||||
händelseloggen, så ingressens gräns måste minst motsvara tjänstens
|
vara upp till 32 MB — lastbalanserarens gräns måste rymma dem,
|
||||||
egen (4 MB) — annars avvisas dokumentationen redan vid dörren.
|
annars avvisas dokumentationen redan vid dörren.
|
||||||
TEXT
|
TEXT
|
||||||
type = number
|
type = number
|
||||||
default = 8
|
default = 64
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "repliker" {
|
variable "repliker" {
|
||||||
@@ -169,143 +97,19 @@ variable "max_repliker" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Databas -----------------------------------------------------------
|
# ---- Självhostad git ---------------------------------------------------
|
||||||
|
|
||||||
variable "databas_lage" {
|
variable "gitea" {
|
||||||
description = <<-TEXT
|
description = <<-TEXT
|
||||||
Hur händelseloggen lagras. Inget standardvärde: valet avgör om det
|
Gitea med egna Actions-runners i samma kluster. Med den på finns
|
||||||
finns säkerhetskopiering, och det är inte ett val någon ska göra av
|
inget externt beroende alls: källkod, bygge, register och drift
|
||||||
misstag.
|
ligger i vår egen AWS-miljö.
|
||||||
|
|
||||||
"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
|
TEXT
|
||||||
type = object({
|
type = object({
|
||||||
mal = string
|
aktiv = optional(bool, true)
|
||||||
endpoint = optional(string, "")
|
doman = optional(string, "")
|
||||||
behall_dagar = optional(number, 30)
|
runners = optional(number, 2)
|
||||||
|
lagring = optional(string, "20Gi")
|
||||||
})
|
})
|
||||||
default = {
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---- Bilagor -----------------------------------------------------------
|
|
||||||
|
|
||||||
variable "bilage_lage" {
|
|
||||||
description = <<-TEXT
|
|
||||||
Var foton och videoklipp lagras.
|
|
||||||
|
|
||||||
"databas" bytea i en egen tabell. Fungerar överallt och kräver
|
|
||||||
ingen konfiguration, men bilderna följer med databasens
|
|
||||||
säkerhetskopior och gör dem stora.
|
|
||||||
|
|
||||||
"s3" S3-kompatibel objektlagring (AWS, MinIO, Ceph). Loggen
|
|
||||||
och bilderna växer oberoende av varandra. Kräver
|
|
||||||
s3-uppgifterna nedan.
|
|
||||||
|
|
||||||
Innehållet är innehållsadresserat i båda lägena: samma foto lagras
|
|
||||||
en gång, och hashen står i händelseloggen så en utbytt bild går att
|
|
||||||
upptäcka.
|
|
||||||
TEXT
|
|
||||||
type = string
|
|
||||||
default = "databas"
|
|
||||||
|
|
||||||
validation {
|
|
||||||
condition = contains(["databas", "s3"], var.bilage_lage)
|
|
||||||
error_message = "bilage_lage måste vara \"databas\" eller \"s3\"."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "s3" {
|
|
||||||
description = "Objektlagring för bilagor. Används bara när bilage_lage = \"s3\"."
|
|
||||||
type = object({
|
|
||||||
endpoint = string
|
|
||||||
hink = string
|
|
||||||
region = string
|
|
||||||
prefix = optional(string, "bilagor")
|
|
||||||
})
|
|
||||||
default = {
|
|
||||||
endpoint = ""
|
|
||||||
hink = ""
|
|
||||||
region = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "s3_nyckel_id" {
|
|
||||||
description = "Åtkomstnyckel till bilagornas objektlagring."
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "s3_nyckel" {
|
|
||||||
description = "Hemlig nyckel till bilagornas objektlagring."
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "databas_storlek" {
|
|
||||||
description = "Volymstorlek för händelseloggen. Foton och video ligger inline i loggen."
|
|
||||||
type = string
|
|
||||||
default = "50Gi"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "lagringsklass" {
|
|
||||||
description = "StorageClass för databasvolymen. Tom = klustrets standard."
|
|
||||||
type = string
|
|
||||||
default = ""
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,83 @@
|
|||||||
# Guidad Felsökning — infrastrukturens definition.
|
# Guidad Felsökning — arbetslasten i klustret.
|
||||||
#
|
#
|
||||||
# Läsordning för den som vill förstå hela systemet:
|
# Lager två av två. Basen (felsokning/infra/aws) måste vara applicerad
|
||||||
# versions.tf det här — leverantörer och tillstånd
|
# först; det här lagret läser dess utdata och behöver därför inte känna
|
||||||
# variables.tf alla rattar som finns att vrida på
|
# till en enda AWS-resurs vid namn.
|
||||||
# karta.tf locals: hela systemet som data (tjänster, portar,
|
#
|
||||||
# routing, hemligheter, dataflöden) — kartans källa
|
# Läsordning:
|
||||||
# 10-namnrymd.tf namnrymd + hemligheter
|
# versions.tf det här — leverantörer, kopplade till EKS
|
||||||
# 20-databas.tf Postgres: händelseloggen
|
# 05-aws.tf basens utdata och vad de betyder här
|
||||||
# 30-plattform.tf backend: auth, händelse-API, delning, integrationer
|
# variables.tf det lilla som inte kommer från basen
|
||||||
# 40-orkester.tf AI-orkestern: modellrouting mot Claude
|
# karta.tf systemet som data — kartans källa
|
||||||
# 50-web.tf klienten
|
# 10-namnrymd.tf namnrymd + hemligheter ur Secrets Manager
|
||||||
# 60-ingress.tf trafiken utifrån och in
|
# 15-plattformstjanster.tf ALB-kontroller, External Secrets, metrics
|
||||||
# 70-natverk.tf nätverkspolicyer: vem får prata med vem
|
# 30-plattform.tf backend
|
||||||
# outputs.tf kartan utskriven — kör `terraform output karta`
|
# 40-orkester.tf AI-orkestern
|
||||||
|
# 50-web.tf klienten
|
||||||
|
# 60-ingress.tf ALB
|
||||||
|
# 70-natverk.tf nätverkspolicyer
|
||||||
|
# 90-gitea.tf självhostad git + byggrunners
|
||||||
|
# outputs.tf kartan utskriven — kör `terraform output karta`
|
||||||
|
|
||||||
terraform {
|
terraform {
|
||||||
required_version = ">= 1.6"
|
required_version = ">= 1.6"
|
||||||
|
|
||||||
required_providers {
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "~> 5.80"
|
||||||
|
}
|
||||||
kubernetes = {
|
kubernetes = {
|
||||||
source = "hashicorp/kubernetes"
|
source = "hashicorp/kubernetes"
|
||||||
version = "~> 2.35"
|
version = "~> 2.35"
|
||||||
}
|
}
|
||||||
|
helm = {
|
||||||
|
source = "hashicorp/helm"
|
||||||
|
version = "~> 2.16"
|
||||||
|
}
|
||||||
random = {
|
random = {
|
||||||
source = "hashicorp/random"
|
source = "hashicorp/random"
|
||||||
version = "~> 3.6"
|
version = "~> 3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Tillståndet innehåller hemligheter (JWT-hemlighet, databaslösenord,
|
# Samma hink som basen, annan nyckel.
|
||||||
# krypteringsnyckeln för kundernas leverantörsuppgifter). Lägg det i en
|
|
||||||
# backend med kryptering och åtkomststyrning — aldrig lokalt i git.
|
|
||||||
#
|
#
|
||||||
# backend "s3" {
|
# backend "s3" {
|
||||||
# bucket = "guidad-felsokning-tfstate"
|
# bucket = "felsokning-produktion-tfstate-<konto>"
|
||||||
# key = "produktion/terraform.tfstate"
|
# key = "arbetslast/terraform.tfstate"
|
||||||
# region = "eu-north-1"
|
# region = "eu-north-1"
|
||||||
# encrypt = true
|
# encrypt = true
|
||||||
# use_lockfile = true
|
# use_lockfile = true
|
||||||
# }
|
# }
|
||||||
}
|
}
|
||||||
|
|
||||||
provider "kubernetes" {
|
provider "aws" {
|
||||||
config_path = var.kubeconfig
|
region = local.aws.region
|
||||||
config_context = var.kube_context
|
}
|
||||||
|
|
||||||
|
# Autentisering mot klustret sker med en färsk token från EKS i stället
|
||||||
|
# för en kubeconfig på disk — inget att distribuera, inget som går ut.
|
||||||
|
provider "kubernetes" {
|
||||||
|
host = local.aws.kluster_slutpunkt
|
||||||
|
cluster_ca_certificate = base64decode(local.aws.kluster_ca)
|
||||||
|
|
||||||
|
exec {
|
||||||
|
api_version = "client.authentication.k8s.io/v1beta1"
|
||||||
|
command = "aws"
|
||||||
|
args = ["eks", "get-token", "--cluster-name", local.aws.kluster, "--region", local.aws.region]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "helm" {
|
||||||
|
kubernetes {
|
||||||
|
host = local.aws.kluster_slutpunkt
|
||||||
|
cluster_ca_certificate = base64decode(local.aws.kluster_ca)
|
||||||
|
|
||||||
|
exec {
|
||||||
|
api_version = "client.authentication.k8s.io/v1beta1"
|
||||||
|
command = "aws"
|
||||||
|
args = ["eks", "get-token", "--cluster-name", local.aws.kluster, "--region", local.aws.region]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user