resource "aws_ecr_repository" "api" { name = local.project image_tag_mutability = "MUTABLE" force_delete = true image_scanning_configuration { scan_on_push = true } encryption_configuration { encryption_type = "AES256" } } resource "aws_ecr_lifecycle_policy" "api" { repository = aws_ecr_repository.api.name policy = jsonencode({ rules = [ { rulePriority = 1 description = "Keep the last 20 images" selection = { tagStatus = "any" countType = "imageCountMoreThan" countNumber = 20 } action = { type = "expire" } } ] }) } resource "aws_security_group" "alb" { name = "${local.project}-alb" description = "ALB for seahaven-ap (CloudFront origin only)" vpc_id = local.vpc_id ingress { description = "HTTP from CloudFront origin-facing prefix list" from_port = 80 to_port = 80 protocol = "tcp" prefix_list_ids = [data.aws_ec2_managed_prefix_list.cloudfront_origin.id] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_security_group" "api" { name = "${local.project}-api" description = "Fargate tasks for seahaven-ap" vpc_id = local.vpc_id ingress { description = "From ALB" from_port = 8080 to_port = 8080 protocol = "tcp" security_groups = [aws_security_group.alb.id] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_lb" "api" { name = local.project load_balancer_type = "application" idle_timeout = 120 security_groups = [aws_security_group.alb.id] subnets = local.public_subnet_ids drop_invalid_header_fields = true } resource "aws_lb_target_group" "api" { name = "${local.project}-api" port = 8080 protocol = "HTTP" vpc_id = local.vpc_id target_type = "ip" health_check { enabled = true path = "/api/health" matcher = "200" interval = 30 timeout = 5 healthy_threshold = 2 unhealthy_threshold = 3 } } resource "aws_lb_listener" "http" { load_balancer_arn = aws_lb.api.arn port = 80 protocol = "HTTP" default_action { type = "forward" target_group_arn = aws_lb_target_group.api.arn } } resource "aws_ecs_cluster" "api" { name = local.project setting { name = "containerInsights" value = "disabled" } } locals { api_container_name = "api" bootstrap_command = [ "node", "-e", "require('http').createServer((q,s)=>{const p=(q.url||'').split('?')[0];const ok=q.method==='GET'&&p==='/api/health';const b=Buffer.from(ok?JSON.stringify({stage:'bootstrap',sha:'bootstrap'}):'');s.writeHead(ok?200:503,ok?{'content-type':'application/json','content-length':b.length}:{});s.end(b)}).listen(8080)", ] database_url = "postgresql://seahaven:${urlencode(random_password.db.result)}@${aws_rds_cluster.api.endpoint}:5432/seahaven_ap" api_environment_map = { NODE_ENV = "production" STAGE = var.environment API_PORT = "8080" DATABASE_DRIVER = "postgres" DATABASE_URL = local.database_url AWS_REGION = var.aws_region COGNITO_ISSUER = local.cognito_issuer COGNITO_AUDIENCE = local.cognito_client_id COGNITO_DOMAIN = local.cognito_hosted_domain APP_ORIGIN = local.app_origin ORIGIN_VERIFY_SECRET = random_password.origin_verify.result DOCUMENTS_BUCKET = aws_s3_bucket.documents.id } api_environment = concat( [for name, value in local.api_environment_map : { name = name, value = value }], [{ name = "GIT_SHA", value = "bootstrap" }], ) } resource "aws_ecs_task_definition" "api" { family = local.project requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" cpu = "512" memory = "1024" execution_role_arn = aws_iam_role.ecs_execution.arn task_role_arn = aws_iam_role.ecs_task.arn runtime_platform { operating_system_family = "LINUX" cpu_architecture = "X86_64" } container_definitions = jsonencode([ { name = local.api_container_name image = "public.ecr.aws/docker/library/node:24-alpine" essential = true command = local.bootstrap_command portMappings = [ { containerPort = 8080 protocol = "tcp" } ] environment = local.api_environment stopTimeout = 60 logConfiguration = { logDriver = "awslogs" options = { "awslogs-group" = aws_cloudwatch_log_group.api.name "awslogs-region" = var.aws_region "awslogs-stream-prefix" = "ecs" } } } ]) lifecycle { ignore_changes = [container_definitions] } } resource "aws_ecs_service" "api" { name = local.project cluster = aws_ecs_cluster.api.id task_definition = aws_ecs_task_definition.api.arn desired_count = 1 launch_type = "FARGATE" network_configuration { subnets = local.public_subnet_ids security_groups = [aws_security_group.api.id] assign_public_ip = true } load_balancer { target_group_arn = aws_lb_target_group.api.arn container_name = local.api_container_name container_port = 8080 } health_check_grace_period_seconds = 60 deployment_minimum_healthy_percent = 0 deployment_maximum_percent = 200 lifecycle { ignore_changes = [task_definition, desired_count] } depends_on = [aws_lb_listener.http] }