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
319 lines
9.1 KiB
Terraform
319 lines
9.1 KiB
Terraform
# 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]
|
|
}
|