AWS-basen: allt eget, inget GitHub
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
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
# 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" }
|
||||
}]
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user