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:
Claude
2026-08-04 12:39:42 +00:00
parent c407aad58a
commit ac24938108
8 changed files with 1632 additions and 6 deletions
+267
View File
@@ -0,0 +1,267 @@
# 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}:*"
}]
})
}
+318
View File
@@ -0,0 +1,318 @@
# Klustret.
#
# Noderna ligger i de privata subnäten och har inga publika adresser.
# API-servern har både en privat och en publik slutpunkt: den privata så
# att noderna når den utan att gå ut, den publika begränsad till
# var.tillatna_api_cidr så att drift och byggrunners kommer åt.
#
# IRSA (IAM Roles for Service Accounts) i stället för nodroller: en pod
# som behöver läsa en hemlighet får en egen roll, och grannpodden på
# samma nod får inte samma rättigheter på köpet.
resource "aws_iam_role" "kluster" {
name = "${local.namn}-kluster"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "eks.amazonaws.com" }
Action = ["sts:AssumeRole", "sts:TagSession"]
}]
})
}
resource "aws_iam_role_policy_attachment" "kluster" {
for_each = toset([
"arn:${data.aws_partition.denna.partition}:iam::aws:policy/AmazonEKSClusterPolicy",
])
role = aws_iam_role.kluster.name
policy_arn = each.value
}
resource "aws_security_group" "kluster" {
name = "${local.namn}-kluster"
description = "EKS kontrollplan."
vpc_id = aws_vpc.denna.id
egress {
description = "Ut till noderna och AWS API"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${local.namn}-kluster" }
}
resource "aws_security_group" "noder" {
name = "${local.namn}-noder"
description = "Arbetsnoder."
vpc_id = aws_vpc.denna.id
ingress {
description = "Noder emellan (pod-till-pod, DNS, kubelet)"
from_port = 0
to_port = 0
protocol = "-1"
self = true
}
ingress {
description = "Kontrollplanet till kubelet och webhooks"
from_port = 1025
to_port = 65535
protocol = "tcp"
security_groups = [aws_security_group.kluster.id]
}
egress {
description = "Ut mot internet via NAT och till VPC-endpoints"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.namn}-noder"
}
}
resource "aws_cloudwatch_log_group" "kluster" {
name = "/aws/eks/${local.namn}/cluster"
retention_in_days = 90
kms_key_id = aws_kms_key.loggar.arn
}
resource "aws_eks_cluster" "denna" {
name = local.namn
role_arn = aws_iam_role.kluster.arn
version = var.kubernetes_version
vpc_config {
subnet_ids = concat(aws_subnet.privat[*].id, aws_subnet.publikt[*].id)
security_group_ids = [aws_security_group.kluster.id]
endpoint_private_access = true
endpoint_public_access = true
public_access_cidrs = var.tillatna_api_cidr
}
# Hemligheter i etcd krypteras med vår egen nyckel, inte AWS delade.
encryption_config {
provider {
key_arn = aws_kms_key.eks.arn
}
resources = ["secrets"]
}
# Revisionsspåret. Utan "audit" går det inte att i efterhand svara på
# vem som gjorde vad i klustret.
enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
access_config {
authentication_mode = "API"
bootstrap_cluster_creator_admin_permissions = true
}
tags = { Name = local.namn }
depends_on = [
aws_iam_role_policy_attachment.kluster,
aws_cloudwatch_log_group.kluster,
]
}
# ---- IRSA ---------------------------------------------------------------
data "tls_certificate" "oidc" {
url = aws_eks_cluster.denna.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "denna" {
url = aws_eks_cluster.denna.identity[0].oidc[0].issuer
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.oidc.certificates[0].sha1_fingerprint]
}
# Hjälpare: förtroendepolicy som binder en roll till EN namnrymd och ETT
# tjänstekonto. Utan sub-villkoret skulle vilket tjänstekonto som helst i
# klustret kunna anta rollen.
locals {
oidc_utfardare = replace(aws_iam_openid_connect_provider.denna.url, "https://", "")
irsa_fortroende = { for k, v in {
plattform = "guidad-felsokning:plattform"
lastbalanserare = "kube-system:aws-load-balancer-controller"
gitea = "gitea:gitea"
} : k => jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.denna.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_utfardare}:aud" = "sts.amazonaws.com"
"${local.oidc_utfardare}:sub" = "system:serviceaccount:${v}"
}
}
}]
}) }
}
# ---- Noder --------------------------------------------------------------
resource "aws_iam_role" "nod" {
name = "${local.namn}-nod"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "nod" {
for_each = toset([
"arn:${data.aws_partition.denna.partition}:iam::aws:policy/AmazonEKSWorkerNodePolicy",
"arn:${data.aws_partition.denna.partition}:iam::aws:policy/AmazonEKS_CNI_Policy",
"arn:${data.aws_partition.denna.partition}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
# Loggar och mätvärden från noden — inte podarnas rättigheter.
"arn:${data.aws_partition.denna.partition}:iam::aws:policy/CloudWatchAgentServerPolicy",
# SSM i stället för SSH: ingen nyckel att tappa bort, och varje
# session hamnar i revisionsloggen.
"arn:${data.aws_partition.denna.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore",
])
role = aws_iam_role.nod.name
policy_arn = each.value
}
resource "aws_launch_template" "nod" {
name_prefix = "${local.namn}-nod-"
vpc_security_group_ids = [aws_security_group.noder.id]
# IMDSv2 obligatoriskt och hoppgräns 1: en pod kan inte nå
# instansmetadata och därmed inte låna nodens roll.
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
}
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = 50
volume_type = "gp3"
encrypted = true
delete_on_termination = true
}
}
monitoring {
enabled = true
}
tag_specifications {
resource_type = "instance"
tags = merge(local.taggar, { Name = "${local.namn}-nod" })
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_eks_node_group" "denna" {
cluster_name = aws_eks_cluster.denna.name
node_group_name = "${local.namn}-noder"
node_role_arn = aws_iam_role.nod.arn
subnet_ids = aws_subnet.privat[*].id
instance_types = var.nod_typer
# Graviton: bättre pris per prestanda. Bilderna byggs för arm64.
ami_type = "AL2023_ARM_64_STANDARD"
scaling_config {
min_size = var.noder.minsta
desired_size = var.noder.onskad
max_size = var.noder.hogsta
}
# En nod i taget ur drift vid uppdatering — PodDisruptionBudget i
# arbetslastlagret ser till att replikerna inte försvinner samtidigt.
update_config {
max_unavailable = 1
}
launch_template {
id = aws_launch_template.nod.id
version = aws_launch_template.nod.latest_version
}
tags = { Name = "${local.namn}-noder" }
depends_on = [aws_iam_role_policy_attachment.nod]
lifecycle {
# Autoskalning ändrar önskat antal — Terraform ska inte dra tillbaka det.
ignore_changes = [scaling_config[0].desired_size]
}
}
# ---- Tillägg ------------------------------------------------------------
#
# Läggs till efter nodgruppen: de behöver noder att köra på.
resource "aws_eks_addon" "denna" {
for_each = toset(["vpc-cni", "coredns", "kube-proxy", "eks-pod-identity-agent"])
cluster_name = aws_eks_cluster.denna.name
addon_name = each.key
resolve_conflicts_on_update = "PRESERVE"
depends_on = [aws_eks_node_group.denna]
}
# EBS-drivrutinen behöver egna rättigheter för att skapa volymer.
resource "aws_iam_role" "ebs" {
name = "${local.namn}-ebs"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.denna.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_utfardare}:aud" = "sts.amazonaws.com"
"${local.oidc_utfardare}:sub" = "system:serviceaccount:kube-system:ebs-csi-controller-sa"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "ebs" {
role = aws_iam_role.ebs.name
policy_arn = "arn:${data.aws_partition.denna.partition}:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
}
resource "aws_eks_addon" "ebs" {
cluster_name = aws_eks_cluster.denna.name
addon_name = "aws-ebs-csi-driver"
service_account_role_arn = aws_iam_role.ebs.arn
resolve_conflicts_on_update = "PRESERVE"
depends_on = [aws_eks_node_group.denna]
}
+302
View File
@@ -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" }
}]
})
}
+308
View File
@@ -0,0 +1,308 @@
# Hemligheterna och vem som får läsa dem.
#
# Secrets Manager i stället för Kubernetes-secrets som sanningskälla:
# rotation, revisionsspår per läsning och kryptering med vår egen nyckel.
# Klustret hämtar dem via IRSA — plattformens tjänstekonto får en egen
# roll, och grannpodden på samma nod får ingenting på köpet.
resource "aws_secretsmanager_secret" "databas" {
name = "${local.namn}/databas"
description = "Aurora-uppgifter för plattformstjänsten."
kms_key_id = aws_kms_key.data.arn
recovery_window_in_days = 30
}
resource "aws_secretsmanager_secret_version" "databas" {
secret_id = aws_secretsmanager_secret.databas.id
secret_string = jsonencode({
username = aws_rds_cluster.denna.master_username
password = random_password.databas.result
host = aws_rds_cluster.denna.endpoint
port = 5432
dbname = aws_rds_cluster.denna.database_name
# Färdig anslutningssträng — plattformstjänsten läser DATABASE_URL.
url = "postgresql://${aws_rds_cluster.denna.master_username}:${random_password.databas.result}@${aws_rds_cluster.denna.endpoint}:5432/${aws_rds_cluster.denna.database_name}?sslmode=require"
})
}
# Applikationens egna hemligheter. Värdena sätts EN gång — Terraform
# äger inte innehållet efteråt, så en rotation i Secrets Manager inte
# skrivs över av nästa apply.
resource "aws_secretsmanager_secret" "app" {
name = "${local.namn}/app"
description = "Claude-nyckel, JWT-hemlighet, krypteringsnyckel för kundernas leverantörsuppgifter."
kms_key_id = aws_kms_key.data.arn
recovery_window_in_days = 30
}
resource "random_password" "jwt" {
length = 64
special = false
}
resource "random_id" "integration" {
byte_length = 32
}
resource "aws_secretsmanager_secret_version" "app" {
secret_id = aws_secretsmanager_secret.app.id
secret_string = jsonencode({
# Fylls i efter första apply — Terraform ska inte känna Claude-nyckeln.
anthropic_api_key = "ERSATT-MIG"
jwt_secret = random_password.jwt.result
integration_nyckel = random_id.integration.hex
})
lifecycle {
ignore_changes = [secret_string]
}
}
# ---- Rollen plattformstjänsten kör som ----------------------------------
resource "aws_iam_role" "plattform" {
name = "${local.namn}-plattform"
assume_role_policy = local.irsa_fortroende["plattform"]
}
resource "aws_iam_role_policy" "plattform" {
name = "las-hemligheter-och-bilagor"
role = aws_iam_role.plattform.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "LasEgnaHemligheter"
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"]
Resource = [
aws_secretsmanager_secret.databas.arn,
aws_secretsmanager_secret.app.arn,
]
},
{
Sid = "Bilagor"
Effect = "Allow"
# Ingen DeleteObject: bilagor hör till en append-only
# händelselogg och tjänsten har ingen anledning att radera dem.
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "${aws_s3_bucket.bilagor.arn}/*"
},
{
Effect = "Allow"
Action = ["s3:ListBucket"]
Resource = aws_s3_bucket.bilagor.arn
},
{
Sid = "KrypteringForOvanstaende"
Effect = "Allow"
Action = ["kms:Decrypt", "kms:GenerateDataKey"]
Resource = aws_kms_key.data.arn
},
]
})
}
# ---- Lastbalanserar-kontrollern -----------------------------------------
#
# Kör i klustret och skapar ALB:er ur Ingress-objekt. Rättigheterna är
# breda till sin natur — den måste kunna skapa och ta bort
# lastbalanserare — men begränsade till den här VPC:n där det går.
resource "aws_iam_role" "lastbalanserare" {
name = "${local.namn}-lastbalanserare"
assume_role_policy = local.irsa_fortroende["lastbalanserare"]
}
resource "aws_iam_role_policy" "lastbalanserare" {
name = "hantera-lastbalanserare"
role = aws_iam_role.lastbalanserare.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"elasticloadbalancing:*",
"ec2:Describe*",
"ec2:CreateSecurityGroup",
"ec2:CreateTags",
"ec2:DeleteTags",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:RevokeSecurityGroupIngress",
"ec2:DeleteSecurityGroup",
"acm:DescribeCertificate",
"acm:ListCertificates",
"iam:CreateServiceLinkedRole",
"cognito-idp:DescribeUserPoolClient",
"wafv2:GetWebACL",
"wafv2:AssociateWebACL",
"shield:GetSubscriptionState",
]
Resource = "*"
},
]
})
}
# ---- Rollerna bygget och driftsättningen antar --------------------------
#
# Byggrunnern får publicera till ECR men inte röra klustret.
# Driftsättningen får röra klustret men inte publicera bilder. Två
# separata roller så ett komprometterat bygge inte kan driftsätta.
resource "aws_iam_role" "bygg" {
name = "${local.namn}-bygg"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.denna.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_utfardare}:aud" = "sts.amazonaws.com"
"${local.oidc_utfardare}:sub" = "system:serviceaccount:gitea:runner"
}
}
}]
})
}
resource "aws_iam_role_policy" "bygg" {
name = "publicera-till-ecr"
role = aws_iam_role.bygg.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "ecr:GetAuthorizationToken"
Resource = "*"
},
{
Effect = "Allow"
Action = [
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart",
"ecr:BatchGetImage",
]
Resource = [for r in aws_ecr_repository.denna : r.arn]
},
{
Effect = "Allow"
Action = ["kms:Encrypt", "kms:GenerateDataKey", "kms:Decrypt"]
Resource = aws_kms_key.data.arn
},
]
})
}
resource "aws_iam_role" "drift" {
name = "${local.namn}-drift"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.denna.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_utfardare}:aud" = "sts.amazonaws.com"
"${local.oidc_utfardare}:sub" = "system:serviceaccount:gitea:drift"
}
}
}]
})
}
resource "aws_iam_role_policy" "drift" {
name = "driftsatt"
role = aws_iam_role.drift.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["eks:DescribeCluster", "eks:ListClusters"]
Resource = aws_eks_cluster.denna.arn
},
{
# Terraform-tillståndet.
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
Resource = [aws_s3_bucket.tillstand.arn, "${aws_s3_bucket.tillstand.arn}/*"]
},
]
})
}
# Driftsättningen behöver också rättigheter i klustret, inte bara i IAM.
resource "aws_eks_access_entry" "drift" {
cluster_name = aws_eks_cluster.denna.name
principal_arn = aws_iam_role.drift.arn
type = "STANDARD"
}
resource "aws_eks_access_policy_association" "drift" {
cluster_name = aws_eks_cluster.denna.name
principal_arn = aws_iam_role.drift.arn
policy_arn = "arn:${data.aws_partition.denna.partition}:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope {
type = "cluster"
}
depends_on = [aws_eks_access_entry.drift]
}
# ---- Terraform-tillståndet ----------------------------------------------
resource "aws_s3_bucket" "tillstand" {
bucket = "${local.namn}-tfstate-${data.aws_caller_identity.denna.account_id}"
tags = { Name = "${local.namn}-tfstate" }
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_public_access_block" "tillstand" {
bucket = aws_s3_bucket.tillstand.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "tillstand" {
bucket = aws_s3_bucket.tillstand.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tillstand" {
bucket = aws_s3_bucket.tillstand.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.data.arn
}
}
}
@@ -0,0 +1,217 @@
# Domän, certifikat och det som gör driften synlig.
# ---- Route 53 och ACM ---------------------------------------------------
data "aws_route53_zone" "befintlig" {
count = var.zon_id != "" ? 1 : 0
zone_id = var.zon_id
}
resource "aws_route53_zone" "ny" {
count = var.zon_id == "" ? 1 : 0
name = join(".", slice(split(".", var.doman), 1, length(split(".", var.doman))))
tags = { Name = local.namn }
}
locals {
zon = var.zon_id != "" ? data.aws_route53_zone.befintlig[0].zone_id : aws_route53_zone.ny[0].zone_id
}
resource "aws_acm_certificate" "denna" {
domain_name = var.doman
validation_method = "DNS"
subject_alternative_names = compact([
var.gitea.aktiv && var.gitea.doman != "" ? var.gitea.doman : null,
])
tags = { Name = local.namn }
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "validering" {
for_each = {
for d in aws_acm_certificate.denna.domain_validation_options : d.domain_name => {
namn = d.resource_record_name
typ = d.resource_record_type
post = d.resource_record_value
}
}
zone_id = local.zon
name = each.value.namn
type = each.value.typ
records = [each.value.post]
ttl = 60
allow_overwrite = true
}
resource "aws_acm_certificate_validation" "denna" {
certificate_arn = aws_acm_certificate.denna.arn
validation_record_fqdns = [for r in aws_route53_record.validering : r.fqdn]
}
# ---- Loggar -------------------------------------------------------------
resource "aws_cloudwatch_log_group" "flodeslogg" {
name = "/aws/vpc/${local.namn}/avvisad"
retention_in_days = 30
kms_key_id = aws_kms_key.loggar.arn
}
resource "aws_cloudwatch_log_group" "applikation" {
name = "/felsokning/${var.miljo}/applikation"
retention_in_days = 90
kms_key_id = aws_kms_key.loggar.arn
}
# ---- Larm ---------------------------------------------------------------
#
# Få larm, men de som finns betyder något. Ett larm som ingen agerar på
# lär folk att ignorera larm.
resource "aws_sns_topic" "larm" {
name = "${local.namn}-larm"
kms_master_key_id = aws_kms_key.loggar.id
}
resource "aws_sns_topic_subscription" "larm" {
count = var.larm_epost != "" ? 1 : 0
topic_arn = aws_sns_topic.larm.arn
protocol = "email"
endpoint = var.larm_epost
}
resource "aws_cloudwatch_metric_alarm" "databas_cpu" {
alarm_name = "${local.namn}-databas-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "CPUUtilization"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = 85
alarm_description = "Aurora ligger högt i tre perioder — skalningstaket kan vara nått."
alarm_actions = [aws_sns_topic.larm.arn]
ok_actions = [aws_sns_topic.larm.arn]
treat_missing_data = "notBreaching"
dimensions = {
DBClusterIdentifier = aws_rds_cluster.denna.cluster_identifier
}
}
resource "aws_cloudwatch_metric_alarm" "databas_lagring" {
alarm_name = "${local.namn}-databas-fri-lagring"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "FreeLocalStorage"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = 5 * 1024 * 1024 * 1024 # 5 GiB
alarm_description = "Lokal lagring håller på att ta slut."
alarm_actions = [aws_sns_topic.larm.arn]
treat_missing_data = "notBreaching"
dimensions = {
DBClusterIdentifier = aws_rds_cluster.denna.cluster_identifier
}
}
# Det viktigaste larmet av alla: att säkerhetskopieringen faktiskt sker.
# En backup man tror finns är värre än ingen.
resource "aws_cloudwatch_metric_alarm" "backup_alder" {
alarm_name = "${local.namn}-backup-alder"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "BackupRetentionPeriodStorageUsed"
namespace = "AWS/RDS"
period = 3600
statistic = "Average"
threshold = 0
alarm_description = "Saknas mätvärdet finns ingen säkerhetskopiering — larmar på saknad data, inte på överskridet tröskelvärde."
alarm_actions = [aws_sns_topic.larm.arn]
treat_missing_data = "breaching"
dimensions = {
DBClusterIdentifier = aws_rds_cluster.denna.cluster_identifier
}
}
resource "aws_cloudwatch_metric_alarm" "noder" {
alarm_name = "${local.namn}-noder-otillrackliga"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "cluster_node_count"
namespace = "ContainerInsights"
period = 300
statistic = "Minimum"
threshold = var.noder.minsta
alarm_description = "Färre noder än minsta önskade — kapacitet saknas eller en zon är nere."
alarm_actions = [aws_sns_topic.larm.arn]
treat_missing_data = "notBreaching"
dimensions = {
ClusterName = aws_eks_cluster.denna.name
}
}
# ---- Instrumentpanel ----------------------------------------------------
resource "aws_cloudwatch_dashboard" "denna" {
dashboard_name = local.namn
dashboard_body = jsonencode({
widgets = [
{
type = "metric", x = 0, y = 0, width = 12, height = 6
properties = {
title = "Databas — belastning"
region = var.region
metrics = [
["AWS/RDS", "CPUUtilization", "DBClusterIdentifier", aws_rds_cluster.denna.cluster_identifier],
[".", "DatabaseConnections", ".", "."],
[".", "ServerlessDatabaseCapacity", ".", "."],
]
period = 300
stat = "Average"
}
},
{
type = "metric", x = 12, y = 0, width = 12, height = 6
properties = {
title = "Klustret"
region = var.region
metrics = [
["ContainerInsights", "cluster_node_count", "ClusterName", aws_eks_cluster.denna.name],
[".", "cluster_failed_node_count", ".", "."],
[".", "pod_cpu_utilization", ".", "."],
]
period = 300
stat = "Average"
}
},
{
type = "metric", x = 0, y = 6, width = 12, height = 6
properties = {
title = "Bilagor i objektlagringen"
region = var.region
metrics = [
["AWS/S3", "BucketSizeBytes", "BucketName", aws_s3_bucket.bilagor.id, "StorageType", "StandardStorage"],
[".", "NumberOfObjects", ".", ".", ".", "AllStorageTypes"],
]
period = 86400
stat = "Average"
}
},
]
})
}
+106
View File
@@ -0,0 +1,106 @@
# AWS-basen
Allt Guidad Felsökning behöver, i eget AWS-konto. Inget GitHub, inget
GHCR, ingen extern byggtjänst.
| Fil | Vad |
| --- | --- |
| `versions.tf` | Leverantörer och tillståndsbackend |
| `variables.tf` | Allt som skiljer en installation från en annan |
| `10-natverk.tf` | VPC, tre subnätlager, NAT, VPC-endpoints, flödesloggar |
| `20-eks.tf` | Klustret, noderna, IRSA, tillägg |
| `30-data.tf` | KMS, Aurora PostgreSQL, S3 för bilagor, ECR |
| `40-hemligheter.tf` | Secrets Manager och rollerna som får läsa dem |
| `50-doman-observation.tf` | Route 53, ACM, CloudWatch, larm |
| `outputs.tf` | Kartan i klartext + värdena arbetslastlagret behöver |
## Två lager, i ordning
```sh
cd felsokning/infra/aws
terraform init && terraform apply # basen
terraform output karta # hela AWS-sidan i klartext
cd ../terraform
terraform init && terraform apply # arbetslasten i klustret
```
Anledningen till 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:
kubernetes-leverantören måste konfigureras med uppgifter som inte finns
förrän klustret är skapat, och planen blir därför opålitlig.
## Vad som gör det till "efter bästa praxis"
**Nätet 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 är ett skydd som inte hänger på att
en säkerhetsgrupp är rätt konfigurerad.
**VPC-endpoints** för S3, ECR, loggar, Secrets Manager, STS och ELB.
Bilddragningar och loggar lämnar aldrig nätet — och NAT-notan sjunker
rejält, eftersom bilddragningar annars dominerar den.
**IRSA i stället för nodroller.** Plattformens tjänstekonto har en egen
roll bunden till exakt en namnrymd och ett tjänstekonto. Grannpodden på
samma nod får ingenting på köpet. `IMDSv2` obligatoriskt med hoppgräns 1
gör dessutom att en pod inte kan låna nodens roll via metadatatjänsten.
**Delade rättigheter mellan bygge och drift.** Byggrollen får publicera
till ECR men inte röra klustret. Driftrollen får röra klustret men inte
publicera bilder. Ett komprometterat bygge kan därför inte driftsätta.
**Egna KMS-nycklar**, tre stycken: en för hemligheter i etcd, en för
databas och bilagor, en för loggar. Åtkomstkontroll och revisionsspår per
nyckel, och möjligheten att rotera en utan att röra de andra.
**Oföränderliga ECR-taggar.** En tagg som pekat på ett bygge kan inte
peka på ett annat, så "vilken kod kör i produktion" har ett entydigt
svar.
**Ingen DeleteObject** för bilagorna. De hör till en append-only
händelselogg, och tjänsten har ingen anledning att kunna radera dem.
**Flödesloggar bara för avvisad trafik.** Allt hade blivit dyrt brus;
avvisningarna är det som säger något när en spärr slår till — eller
borde ha gjort det.
**Larm som betyder något.** Fyra stycken, varav ett 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.
## Efter första apply
`terraform output karta` listar det under `kvar_att_gora`:
1. **Claude-nyckeln.** Terraform ska inte känna den.
```sh
aws secretsmanager put-secret-value \
--secret-id felsokning-produktion/app \
--secret-string "$(aws secretsmanager get-secret-value --secret-id felsokning-produktion/app \
--query SecretString --output text | jq '.anthropic_api_key = "sk-ant-…"')"
```
2. **Schemat.** Kör `felsokning/infra/postgres-init.sql` mot Aurora — det
är den som lägger in append-only-triggarna. Samma fil som
integrationstestet kör, så de kan inte glida isär.
3. **Begränsa `tillatna_api_cidr`.** Standard är öppet mot internet
eftersom klustret annars inte går att nå från en ny miljö. Snäva in
till kontorets och runnernas adresser.
4. **Sätt `larm_epost`.** Utan den går larmen ingenstans.
## Kostnad — det som faktiskt kostar
NAT-gateways (en per zon), Auroras minsta kapacitet och EKS
kontrollplan är golvet och löper oavsett last. `nat_per_zon = false`
halverar nätkostnaden i test men gör en zon till enskild felkälla för
all utgående trafik — fel avvägning i produktion.
## Verifiering
`terraform validate` kunde inte köras i utvecklingsmiljön —
registry.terraform.io är blockerad av sessionens egress-policy, så
leverantörerna gick inte att hämta. Det som kontrollerats är
`terraform fmt` och en statisk referenskontroll (88 resurser, 5
datakällor, inga dinglande referenser, alla `count`-resurser indexerade).
Kör `terraform init -backend=false && terraform validate` innan första
apply.
+108
View File
@@ -0,0 +1,108 @@
# Det arbetslastlagret behöver, och kartan i klartext.
#
# terraform output karta hela AWS-basen
# terraform output -json for_arbetslast matas in i felsokning/infra/terraform
output "karta" {
description = "AWS-basen: nät, kluster, data, hemligheter och gränser."
value = {
region = var.region
miljo = var.miljo
doman = "https://${var.doman}"
nat = {
vpc = "${aws_vpc.denna.id} (${var.vpc_cidr})"
zoner = local.zoner
publikt = "bara lastbalanserare och NAT"
privat = "noderna — inga publika adresser"
data = "Aurora — ingen väg ut alls"
nat_gateways = var.nat_per_zon ? "en per zon" : "en delad (ej produktion)"
endpoints = "S3 via gateway; ECR, loggar, Secrets Manager, STS och ELB via gränssnitt — trafiken lämnar aldrig nätet"
}
kluster = {
namn = aws_eks_cluster.denna.name
version = aws_eks_cluster.denna.version
slutpunkt = aws_eks_cluster.denna.endpoint
noder = "${var.noder.minsta}${var.noder.hogsta} × ${join(", ", var.nod_typer)} (arm64)"
auth = "IRSA — varje tjänstekonto har egen roll; noderna delar inga rättigheter"
loggar = "api, audit, authenticator, controllerManager, scheduler"
metadata = "IMDSv2 obligatoriskt, hoppgräns 1 — podar kan inte låna nodens roll"
}
databas = {
motor = "Aurora PostgreSQL ${aws_rds_cluster.denna.engine_version} Serverless v2"
kapacitet = "${var.databas.min_kapacitet}${var.databas.max_kapacitet} ACU × ${var.databas.instanser} instanser"
slutpunkt = aws_rds_cluster.denna.endpoint
backup = "${var.databas.backup_dagar} dagar med PITR ned till sekunden"
kryptering = "KMS med egen nyckel, i vila och över nätet (sslmode=require)"
atkomst = "endast från klustrets noder, i ett subnätlager utan routing ut"
}
bilagor = {
hink = aws_s3_bucket.bilagor.id
kryptering = "SSE-KMS med egen nyckel"
skydd = "publik åtkomst blockerad, TLS obligatoriskt, versionshantering på"
atkomst = "plattformens roll får läsa och skriva — men aldrig radera"
}
register = {
for namn, r in aws_ecr_repository.denna : namn => "${r.repository_url} (oföränderliga taggar, sårbarhetsskanning)"
}
hemligheter = {
databas = aws_secretsmanager_secret.databas.name
app = "${aws_secretsmanager_secret.app.name} — Claude-nyckeln fylls i för hand efter första apply"
lasare = "endast rollen ${aws_iam_role.plattform.name}, via IRSA"
}
roller = {
plattform = aws_iam_role.plattform.arn
lastbalanserare = aws_iam_role.lastbalanserare.arn
bygg = "${aws_iam_role.bygg.arn} — får publicera till ECR, får inte röra klustret"
drift = "${aws_iam_role.drift.arn} — får röra klustret, får inte publicera bilder"
}
observation = {
instrumentpanel = "https://${var.region}.console.aws.amazon.com/cloudwatch/home?region=${var.region}#dashboards:name=${aws_cloudwatch_dashboard.denna.dashboard_name}"
larm = var.larm_epost != "" ? "till ${var.larm_epost}" : "INGEN mottagare satt — larm går ingenstans"
flodesloggar = "avvisad trafik, 30 dagar"
}
kvar_att_gora = compact([
"Fyll i Claude-nyckeln: aws secretsmanager put-secret-value --secret-id ${aws_secretsmanager_secret.app.name}",
"Kör felsokning/infra/postgres-init.sql mot databasen — schemat med append-only-triggarna.",
"Kör arbetslastlagret: cd ../terraform && terraform apply",
var.larm_epost == "" ? "Sätt larm_epost — annars larmar ingenting till någon." : "",
var.tillatna_api_cidr == ["0.0.0.0/0"] ? "Begränsa tillatna_api_cidr — API-servern är öppen mot hela internet." : "",
])
}
}
# Matas in i arbetslastlagret så det inte behöver upprepa någonting.
output "for_arbetslast" {
description = "Värden som felsokning/infra/terraform behöver."
value = {
region = var.region
kluster = aws_eks_cluster.denna.name
kluster_slutpunkt = aws_eks_cluster.denna.endpoint
kluster_ca = aws_eks_cluster.denna.certificate_authority[0].data
doman = var.doman
certifikat_arn = aws_acm_certificate_validation.denna.certificate_arn
register = split("/", aws_ecr_repository.denna["guidad-felsokning-web"].repository_url)[0]
plattform_roll = aws_iam_role.plattform.arn
lastbalanserar_roll = aws_iam_role.lastbalanserare.arn
bilage_hink = aws_s3_bucket.bilagor.id
hemlighet_databas = aws_secretsmanager_secret.databas.name
hemlighet_app = aws_secretsmanager_secret.app.name
zon_id = local.zon
}
}
output "databas_url" {
description = "Anslutningssträng. Hämtas normalt ur Secrets Manager — den här är för engångskörning av schemat."
sensitive = true
value = "postgresql://${aws_rds_cluster.denna.master_username}:${random_password.databas.result}@${aws_rds_cluster.denna.endpoint}:5432/${aws_rds_cluster.denna.database_name}?sslmode=require"
}