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
309 lines
8.9 KiB
Terraform
309 lines
8.9 KiB
Terraform
# 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
|
|
}
|
|
}
|
|
}
|