Files
alva/felsokning/infra/aws/50-doman-observation.tf
T
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

218 lines
6.6 KiB
Terraform

# 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"
}
},
]
})
}