Files
Claude ac24938108 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
2026-08-04 12:39:42 +00:00

268 lines
7.3 KiB
Terraform

# Nätet.
#
# Tre lager subnät per zon:
# publikt bara lastbalanserare och NAT-gateway
# privat noderna — inga publika IP-adresser
# data Aurora, helt utan väg ut
#
# Databasen får ett eget lager i stället för att dela nodernas subnät.
# Det gör "vem kan nå databasen" till en fråga om routing och inte bara
# om säkerhetsgrupper — två oberoende spärrar i stället för en.
locals {
namn = "felsokning-${var.miljo}"
taggar = {
Projekt = "guidad-felsokning"
Miljo = var.miljo
Forvaltas = "terraform"
}
zoner = slice(data.aws_availability_zones.tillgangliga.names, 0, var.antal_zoner)
# /20 per subnät ur en /16: 4096 adresser, gott om plats för noder som
# var och en tar ett hundratal ENI-adresser.
publika_cidr = [for i in range(var.antal_zoner) : cidrsubnet(var.vpc_cidr, 4, i)]
privata_cidr = [for i in range(var.antal_zoner) : cidrsubnet(var.vpc_cidr, 4, i + 4)]
data_cidr = [for i in range(var.antal_zoner) : cidrsubnet(var.vpc_cidr, 4, i + 8)]
}
data "aws_availability_zones" "tillgangliga" {
state = "available"
filter {
name = "opt-in-status"
values = ["opt-in-not-required"]
}
}
resource "aws_vpc" "denna" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true # krävs av EKS och av VPC-endpoints
tags = { Name = local.namn }
}
resource "aws_internet_gateway" "denna" {
vpc_id = aws_vpc.denna.id
tags = { Name = local.namn }
}
# ---- Subnät -------------------------------------------------------------
resource "aws_subnet" "publikt" {
count = var.antal_zoner
vpc_id = aws_vpc.denna.id
cidr_block = local.publika_cidr[count.index]
availability_zone = local.zoner[count.index]
map_public_ip_on_launch = false # bara lastbalanserare här — de får sina egna
tags = {
Name = "${local.namn}-publikt-${local.zoner[count.index]}"
# Taggarna är inte kosmetiska: lastbalanserar-kontrollern hittar
# subnäten genom dem.
"kubernetes.io/role/elb" = "1"
}
}
resource "aws_subnet" "privat" {
count = var.antal_zoner
vpc_id = aws_vpc.denna.id
cidr_block = local.privata_cidr[count.index]
availability_zone = local.zoner[count.index]
tags = {
Name = "${local.namn}-privat-${local.zoner[count.index]}"
"kubernetes.io/role/internal-elb" = "1"
}
}
resource "aws_subnet" "data" {
count = var.antal_zoner
vpc_id = aws_vpc.denna.id
cidr_block = local.data_cidr[count.index]
availability_zone = local.zoner[count.index]
tags = { Name = "${local.namn}-data-${local.zoner[count.index]}" }
}
# ---- Utgående trafik ----------------------------------------------------
resource "aws_eip" "nat" {
count = var.nat_per_zon ? var.antal_zoner : 1
domain = "vpc"
tags = { Name = "${local.namn}-nat-${count.index}" }
depends_on = [aws_internet_gateway.denna]
}
resource "aws_nat_gateway" "denna" {
count = var.nat_per_zon ? var.antal_zoner : 1
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.publikt[count.index].id
tags = { Name = "${local.namn}-${count.index}" }
depends_on = [aws_internet_gateway.denna]
}
# ---- Routing ------------------------------------------------------------
resource "aws_route_table" "publikt" {
vpc_id = aws_vpc.denna.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.denna.id
}
tags = { Name = "${local.namn}-publikt" }
}
resource "aws_route_table_association" "publikt" {
count = var.antal_zoner
subnet_id = aws_subnet.publikt[count.index].id
route_table_id = aws_route_table.publikt.id
}
resource "aws_route_table" "privat" {
count = var.antal_zoner
vpc_id = aws_vpc.denna.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.denna[var.nat_per_zon ? count.index : 0].id
}
tags = { Name = "${local.namn}-privat-${local.zoner[count.index]}" }
}
resource "aws_route_table_association" "privat" {
count = var.antal_zoner
subnet_id = aws_subnet.privat[count.index].id
route_table_id = aws_route_table.privat[count.index].id
}
# Datalagret har ingen väg ut. Aurora behöver ingen, och saknaden är
# skyddet: en komprometterad databas kan inte ringa hem.
resource "aws_route_table" "data" {
vpc_id = aws_vpc.denna.id
tags = { Name = "${local.namn}-data" }
}
resource "aws_route_table_association" "data" {
count = var.antal_zoner
subnet_id = aws_subnet.data[count.index].id
route_table_id = aws_route_table.data.id
}
# ---- VPC-endpoints ------------------------------------------------------
#
# Trafik till AWS egna tjänster går inom nätet i stället för ut genom NAT.
# Två vinster som drar åt samma håll: bilder och loggar passerar aldrig
# internet, och NAT-notan sjunker rejält — bilddragningar dominerar annars.
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.denna.id
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = concat(aws_route_table.privat[*].id, [aws_route_table.data.id])
tags = { Name = "${local.namn}-s3" }
}
resource "aws_security_group" "endpoints" {
name = "${local.namn}-endpoints"
description = "Interface-endpoints — tar emot HTTPS från noderna."
vpc_id = aws_vpc.denna.id
ingress {
description = "HTTPS från noderna"
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.noder.id]
}
tags = { Name = "${local.namn}-endpoints" }
}
resource "aws_vpc_endpoint" "granssnitt" {
for_each = toset([
"ecr.api",
"ecr.dkr",
"logs",
"secretsmanager",
"sts",
"elasticloadbalancing",
])
vpc_id = aws_vpc.denna.id
service_name = "com.amazonaws.${var.region}.${each.key}"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.privat[*].id
security_group_ids = [aws_security_group.endpoints.id]
private_dns_enabled = true
tags = { Name = "${local.namn}-${replace(each.key, ".", "-")}" }
}
# ---- Flödesloggar -------------------------------------------------------
#
# Bara avvisad trafik. Allt hade blivit dyrt brus; avvisningarna är det
# som faktiskt säger något när en spärr slår till eller borde ha gjort det.
resource "aws_flow_log" "denna" {
vpc_id = aws_vpc.denna.id
traffic_type = "REJECT"
iam_role_arn = aws_iam_role.flodeslogg.arn
log_destination = aws_cloudwatch_log_group.flodeslogg.arn
max_aggregation_interval = 60
tags = { Name = local.namn }
}
resource "aws_iam_role" "flodeslogg" {
name = "${local.namn}-flodeslogg"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "vpc-flow-logs.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "flodeslogg" {
name = "skriv-loggar"
role = aws_iam_role.flodeslogg.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
]
Resource = "${aws_cloudwatch_log_group.flodeslogg.arn}:*"
}]
})
}