ac24938108
Guidad Felsökning får en egen AWS-grund i felsokning/infra/aws — nät, kluster, databas, objektlagring, register, hemligheter, domän och observation. Inget GHCR, ingen extern byggtjänst. Två lager i ordning, och uppdelningen är inte smak: en enda apply som både skapar ett EKS-kluster och schemalägger in i det är en känd fälla, eftersom kubernetes-leverantören måste konfigureras med uppgifter som inte finns förrän klustret existerar. Nätet ligger i tre lager. Publikt bara för lastbalanserare och NAT, privat för noderna, och ett eget datalager för Aurora utan routing ut alls — att databasen inte kan nå internet hänger då inte på att en säkerhetsgrupp är rätt konfigurerad. VPC-endpoints för S3, ECR, loggar, Secrets Manager, STS och ELB gör att bilddragningar och loggar aldrig lämnar nätet, vilket både är säkrare och sänker NAT-notan rejält. IRSA i stället för nodroller: plattformens tjänstekonto har en roll bunden till exakt en namnrymd och ett tjänstekonto, så grannpodden på samma nod får ingenting på köpet. IMDSv2 med hoppgräns 1 hindrar dessutom en pod från att låna nodens roll via metadatatjänsten. Bygge och drift har delade rättigheter. Byggrollen får publicera till ECR men inte röra klustret; driftrollen tvärtom. Ett komprometterat bygge kan därför inte driftsätta. Aurora PostgreSQL Serverless v2 med 30 dagars säkerhetskopiering och PITR ned till sekunden — det som gör den duglig för händelseloggen är inte prestandan utan att förlorad logg annars hade tagit varje ärendes bevisvärde med sig. S3-hinken för bilagor har versionshantering, SSE-KMS, TLS-krav och blockerad publik åtkomst; plattformens roll får läsa och skriva men aldrig radera, eftersom bilagorna hör till en append-only logg. Larmen är fyra och ett av dem larmar på saknad data: uteblir mätvärdet för säkerhetskopiering finns ingen backup, och en backup man tror finns är värre än ingen. Verifierat: terraform fmt och en statisk referenskontroll (88 resurser, 5 datakällor, inga dinglande referenser). terraform validate kunde inte köras här — registry.terraform.io är blockerad av sessionens egress-policy — så CI-jobbet kör den i stället. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
303 lines
8.5 KiB
Terraform
303 lines
8.5 KiB
Terraform
# Data: händelseloggen, bilagorna och bilderna.
|
|
|
|
# ---- Nycklar ------------------------------------------------------------
|
|
#
|
|
# Egna KMS-nycklar i stället för AWS delade. Det som vinns är
|
|
# åtkomstkontroll och revisionsspår per nyckel — och möjligheten att
|
|
# rotera eller spärra utan att röra något annat.
|
|
|
|
resource "aws_kms_key" "eks" {
|
|
description = "${local.namn} — hemligheter i etcd"
|
|
enable_key_rotation = true
|
|
deletion_window_in_days = 30
|
|
}
|
|
|
|
resource "aws_kms_alias" "eks" {
|
|
name = "alias/${local.namn}-eks"
|
|
target_key_id = aws_kms_key.eks.key_id
|
|
}
|
|
|
|
resource "aws_kms_key" "data" {
|
|
description = "${local.namn} — databas och bilagor"
|
|
enable_key_rotation = true
|
|
deletion_window_in_days = 30
|
|
}
|
|
|
|
resource "aws_kms_alias" "data" {
|
|
name = "alias/${local.namn}-data"
|
|
target_key_id = aws_kms_key.data.key_id
|
|
}
|
|
|
|
resource "aws_kms_key" "loggar" {
|
|
description = "${local.namn} — loggar"
|
|
enable_key_rotation = true
|
|
deletion_window_in_days = 30
|
|
|
|
# CloudWatch måste kunna kryptera med nyckeln.
|
|
policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Effect = "Allow"
|
|
Principal = { AWS = "arn:${data.aws_partition.denna.partition}:iam::${data.aws_caller_identity.denna.account_id}:root" }
|
|
Action = "kms:*"
|
|
Resource = "*"
|
|
},
|
|
{
|
|
Effect = "Allow"
|
|
Principal = { Service = "logs.${var.region}.amazonaws.com" }
|
|
Action = ["kms:Encrypt*", "kms:Decrypt*", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:Describe*"]
|
|
Resource = "*"
|
|
Condition = {
|
|
ArnLike = {
|
|
"kms:EncryptionContext:aws:logs:arn" = "arn:${data.aws_partition.denna.partition}:logs:${var.region}:${data.aws_caller_identity.denna.account_id}:log-group:*"
|
|
}
|
|
}
|
|
},
|
|
]
|
|
})
|
|
}
|
|
|
|
resource "aws_kms_alias" "loggar" {
|
|
name = "alias/${local.namn}-loggar"
|
|
target_key_id = aws_kms_key.loggar.key_id
|
|
}
|
|
|
|
# ---- Aurora PostgreSQL --------------------------------------------------
|
|
#
|
|
# Serverless v2 skalar med lasten. Det som gör att den här databasen
|
|
# duger till händelseloggen är inte prestandan utan
|
|
# säkerhetskopieringen: automatisk backup med PITR ned till sekunden
|
|
# inom retentionsfönstret. Går loggen förlorad är varje ärendes
|
|
# bevisvärde borta, och det går inte att återskapa i efterhand.
|
|
|
|
resource "aws_db_subnet_group" "denna" {
|
|
name = local.namn
|
|
subnet_ids = aws_subnet.data[*].id
|
|
|
|
tags = { Name = local.namn }
|
|
}
|
|
|
|
resource "aws_security_group" "databas" {
|
|
name = "${local.namn}-databas"
|
|
description = "Aurora — tar bara emot från noderna."
|
|
vpc_id = aws_vpc.denna.id
|
|
|
|
ingress {
|
|
description = "PostgreSQL från klustrets noder"
|
|
from_port = 5432
|
|
to_port = 5432
|
|
protocol = "tcp"
|
|
security_groups = [aws_security_group.noder.id]
|
|
}
|
|
|
|
# Ingen egress-regel: databasen ska inte kunna ringa någonstans.
|
|
|
|
tags = { Name = "${local.namn}-databas" }
|
|
}
|
|
|
|
resource "random_password" "databas" {
|
|
length = 40
|
|
special = false # undviker citeringsproblem i anslutningssträngar
|
|
}
|
|
|
|
resource "aws_rds_cluster" "denna" {
|
|
cluster_identifier = local.namn
|
|
engine = "aurora-postgresql"
|
|
engine_mode = "provisioned"
|
|
engine_version = var.databas.version
|
|
database_name = "felsokning"
|
|
master_username = "plattform"
|
|
master_password = random_password.databas.result
|
|
|
|
db_subnet_group_name = aws_db_subnet_group.denna.name
|
|
vpc_security_group_ids = [aws_security_group.databas.id]
|
|
|
|
storage_encrypted = true
|
|
kms_key_id = aws_kms_key.data.arn
|
|
|
|
backup_retention_period = var.databas.backup_dagar
|
|
preferred_backup_window = "02:00-03:00"
|
|
preferred_maintenance_window = "sun:03:30-sun:04:30"
|
|
copy_tags_to_snapshot = true
|
|
|
|
deletion_protection = var.databas.raderingsskydd
|
|
skip_final_snapshot = false
|
|
final_snapshot_identifier = "${local.namn}-slutlig"
|
|
|
|
# Loggarna behövs för att i efterhand kunna se vad som kördes.
|
|
enabled_cloudwatch_logs_exports = ["postgresql"]
|
|
|
|
serverlessv2_scaling_configuration {
|
|
min_capacity = var.databas.min_kapacitet
|
|
max_capacity = var.databas.max_kapacitet
|
|
}
|
|
|
|
tags = { Name = local.namn }
|
|
|
|
lifecycle {
|
|
ignore_changes = [master_password] # roteras via Secrets Manager
|
|
}
|
|
}
|
|
|
|
resource "aws_rds_cluster_instance" "denna" {
|
|
count = var.databas.instanser
|
|
|
|
identifier = "${local.namn}-${count.index}"
|
|
cluster_identifier = aws_rds_cluster.denna.id
|
|
instance_class = "db.serverless"
|
|
engine = aws_rds_cluster.denna.engine
|
|
engine_version = aws_rds_cluster.denna.engine_version
|
|
publicly_accessible = false
|
|
|
|
performance_insights_enabled = true
|
|
performance_insights_kms_key_id = aws_kms_key.data.arn
|
|
monitoring_interval = 30
|
|
monitoring_role_arn = aws_iam_role.rds_overvakning.arn
|
|
|
|
tags = { Name = "${local.namn}-${count.index}" }
|
|
}
|
|
|
|
resource "aws_iam_role" "rds_overvakning" {
|
|
name = "${local.namn}-rds-overvakning"
|
|
|
|
assume_role_policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [{
|
|
Effect = "Allow"
|
|
Principal = { Service = "monitoring.rds.amazonaws.com" }
|
|
Action = "sts:AssumeRole"
|
|
}]
|
|
})
|
|
}
|
|
|
|
resource "aws_iam_role_policy_attachment" "rds_overvakning" {
|
|
role = aws_iam_role.rds_overvakning.name
|
|
policy_arn = "arn:${data.aws_partition.denna.partition}:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
|
|
}
|
|
|
|
# ---- S3 för bilagor -----------------------------------------------------
|
|
#
|
|
# Foton och videoklipp. Innehållsadresserade — nyckeln är innehållets
|
|
# SHA-256 — så versionshantering skyddar mot oavsiktlig radering snarare
|
|
# än mot överskrivning: samma nyckel har alltid samma innehåll.
|
|
|
|
resource "aws_s3_bucket" "bilagor" {
|
|
bucket = "${local.namn}-bilagor-${data.aws_caller_identity.denna.account_id}"
|
|
|
|
tags = { Name = "${local.namn}-bilagor" }
|
|
}
|
|
|
|
resource "aws_s3_bucket_public_access_block" "bilagor" {
|
|
bucket = aws_s3_bucket.bilagor.id
|
|
|
|
block_public_acls = true
|
|
block_public_policy = true
|
|
ignore_public_acls = true
|
|
restrict_public_buckets = true
|
|
}
|
|
|
|
resource "aws_s3_bucket_versioning" "bilagor" {
|
|
bucket = aws_s3_bucket.bilagor.id
|
|
|
|
versioning_configuration {
|
|
status = "Enabled"
|
|
}
|
|
}
|
|
|
|
resource "aws_s3_bucket_server_side_encryption_configuration" "bilagor" {
|
|
bucket = aws_s3_bucket.bilagor.id
|
|
|
|
rule {
|
|
apply_server_side_encryption_by_default {
|
|
sse_algorithm = "aws:kms"
|
|
kms_master_key_id = aws_kms_key.data.arn
|
|
}
|
|
bucket_key_enabled = true # färre KMS-anrop, lägre kostnad
|
|
}
|
|
}
|
|
|
|
resource "aws_s3_bucket_lifecycle_configuration" "bilagor" {
|
|
bucket = aws_s3_bucket.bilagor.id
|
|
|
|
rule {
|
|
id = "stada-gamla-versioner"
|
|
status = "Enabled"
|
|
|
|
filter {}
|
|
|
|
noncurrent_version_expiration {
|
|
noncurrent_days = 90
|
|
}
|
|
|
|
abort_incomplete_multipart_upload {
|
|
days_after_initiation = 7
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_s3_bucket_policy" "bilagor" {
|
|
bucket = aws_s3_bucket.bilagor.id
|
|
|
|
policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [{
|
|
Sid = "KravTLS"
|
|
Effect = "Deny"
|
|
Principal = "*"
|
|
Action = "s3:*"
|
|
Resource = [aws_s3_bucket.bilagor.arn, "${aws_s3_bucket.bilagor.arn}/*"]
|
|
Condition = { Bool = { "aws:SecureTransport" = "false" } }
|
|
}]
|
|
})
|
|
|
|
depends_on = [aws_s3_bucket_public_access_block.bilagor]
|
|
}
|
|
|
|
# ---- ECR ----------------------------------------------------------------
|
|
#
|
|
# Vårt eget register. Oföränderliga taggar: en tagg som en gång pekat på
|
|
# ett bygge kan inte peka på ett annat, så "vilken kod kör i produktion"
|
|
# har ett entydigt svar.
|
|
|
|
resource "aws_ecr_repository" "denna" {
|
|
for_each = toset([
|
|
"guidad-felsokning-web",
|
|
"guidad-felsokning-plattform",
|
|
"guidad-felsokning-ai-orkester",
|
|
])
|
|
|
|
name = each.key
|
|
image_tag_mutability = "IMMUTABLE"
|
|
|
|
image_scanning_configuration {
|
|
scan_on_push = true
|
|
}
|
|
|
|
encryption_configuration {
|
|
encryption_type = "KMS"
|
|
kms_key = aws_kms_key.data.arn
|
|
}
|
|
|
|
tags = { Name = each.key }
|
|
}
|
|
|
|
resource "aws_ecr_lifecycle_policy" "denna" {
|
|
for_each = aws_ecr_repository.denna
|
|
|
|
repository = each.value.name
|
|
|
|
policy = jsonencode({
|
|
rules = [{
|
|
rulePriority = 1
|
|
description = "Behåll de 30 senaste bilderna."
|
|
selection = {
|
|
tagStatus = "any"
|
|
countType = "imageCountMoreThan"
|
|
countNumber = 30
|
|
}
|
|
action = { type = "expire" }
|
|
}]
|
|
})
|
|
}
|