terraform {
  required_version = ">= 1.6, < 2.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "6.54.0" }
  }
}
variable "region" { type = string }
variable "account_id" { type = string }
variable "name" {
  type = string
  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{2,19}$", var.name))
    error_message = "Use 3-20 lowercase letters, numbers or hyphens."
  }
}
variable "azs" {
  type = list(string)
  validation {
    condition     = length(var.azs) == 2 && length(distinct(var.azs)) == 2
    error_message = "Choose two distinct AZs in the target region."
  }
}
variable "ami_id" { type = string }
variable "certificate_arn" { type = string }
variable "client_cidr" {
  type = string
  validation {
    condition     = can(cidrnetmask(var.client_cidr)) && !endswith(var.client_cidr, "/0")
    error_message = "Use an approved IPv4 client range, not /0."
  }
}
variable "orders_image" {
  type = string
  validation {
    condition     = can(regex("^[0-9]{12}\\.dkr\\.ecr\\.[a-z0-9-]+\\.amazonaws\\.com/[a-z0-9/_-]+@sha256:[a-f0-9]{64}$", var.orders_image))
    error_message = "Use a private ECR image pinned by sha256 digest."
  }
}
variable "ecr_repository_arn" { type = string }
variable "db_engine_version" {
  type = string
  validation {
    condition     = can(regex("^17\\.[0-9]+$", var.db_engine_version))
    error_message = "Choose an approved PostgreSQL 17 minor version available in this region."
  }
}
variable "final_snapshot_id" { type = string }
variable "deletion_protection" {
  type    = bool
  default = true
}
provider "aws" {
  region              = var.region
  allowed_account_ids = [var.account_id]
  default_tags { tags = { Project = var.name, Environment = "preproduction", ManagedBy = "terraform" } }
}
locals {
  zones = { for i, az in var.azs : az => i }
}
resource "aws_vpc" "main" {
  cidr_block           = "10.64.0.0/18"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags                 = { Name = var.name }
}
resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id }
resource "aws_subnet" "public" {
  for_each          = local.zones
  vpc_id            = aws_vpc.main.id
  availability_zone = each.key
  cidr_block        = "10.64.${9 + 16 * each.value}.0/24"
  tags              = { Name = "${var.name}-public-${each.value}" }
}
resource "aws_subnet" "app" {
  for_each          = local.zones
  vpc_id            = aws_vpc.main.id
  availability_zone = each.key
  cidr_block        = "10.64.${16 * each.value}.0/21"
  tags              = { Name = "${var.name}-app-${each.value}" }
}
resource "aws_subnet" "data" {
  for_each          = local.zones
  vpc_id            = aws_vpc.main.id
  availability_zone = each.key
  cidr_block        = "10.64.${8 + 16 * each.value}.0/24"
  tags              = { Name = "${var.name}-data-${each.value}" }
}
resource "aws_route_table" "public" { vpc_id = aws_vpc.main.id }
resource "aws_route" "internet" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.main.id
}
resource "aws_route_table_association" "public" {
  for_each       = local.zones
  subnet_id      = aws_subnet.public[each.key].id
  route_table_id = aws_route_table.public.id
}
resource "aws_eip" "nat" {
  for_each = local.zones
  domain   = "vpc"
}
resource "aws_nat_gateway" "main" {
  for_each      = local.zones
  allocation_id = aws_eip.nat[each.key].id
  subnet_id     = aws_subnet.public[each.key].id
  depends_on    = [aws_route.internet]
}
resource "aws_route_table" "app" {
  for_each = local.zones
  vpc_id   = aws_vpc.main.id
}
resource "aws_route" "nat" {
  for_each               = local.zones
  route_table_id         = aws_route_table.app[each.key].id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.main[each.key].id
}
resource "aws_route_table_association" "app" {
  for_each       = local.zones
  subnet_id      = aws_subnet.app[each.key].id
  route_table_id = aws_route_table.app[each.key].id
}
resource "aws_route_table" "data" { vpc_id = aws_vpc.main.id }
resource "aws_route_table_association" "data" {
  for_each       = local.zones
  subnet_id      = aws_subnet.data[each.key].id
  route_table_id = aws_route_table.data.id
}
resource "aws_security_group" "alb" {
  name   = "${var.name}-alb"
  vpc_id = aws_vpc.main.id
}
resource "aws_security_group" "app" {
  name   = "${var.name}-app"
  vpc_id = aws_vpc.main.id
}
resource "aws_security_group" "db" {
  name   = "${var.name}-db"
  vpc_id = aws_vpc.main.id
}
resource "aws_vpc_security_group_ingress_rule" "https" {
  security_group_id = aws_security_group.alb.id
  cidr_ipv4         = var.client_cidr
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
}
resource "aws_vpc_security_group_egress_rule" "alb_app" {
  security_group_id            = aws_security_group.alb.id
  referenced_security_group_id = aws_security_group.app.id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080
}
resource "aws_vpc_security_group_ingress_rule" "app" {
  security_group_id            = aws_security_group.app.id
  referenced_security_group_id = aws_security_group.alb.id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080
}
resource "aws_vpc_security_group_egress_rule" "app_db" {
  security_group_id            = aws_security_group.app.id
  referenced_security_group_id = aws_security_group.db.id
  ip_protocol                  = "tcp"
  from_port                    = 5432
  to_port                      = 5432
}
resource "aws_vpc_security_group_egress_rule" "app_https" {
  security_group_id = aws_security_group.app.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
}
resource "aws_vpc_security_group_ingress_rule" "db" {
  security_group_id            = aws_security_group.db.id
  referenced_security_group_id = aws_security_group.app.id
  ip_protocol                  = "tcp"
  from_port                    = 5432
  to_port                      = 5432
}
resource "aws_db_subnet_group" "main" {
  name       = var.name
  subnet_ids = [for s in aws_subnet.data : s.id]
}
resource "aws_db_parameter_group" "main" {
  name   = var.name
  family = "postgres17"
  parameter {
    name  = "rds.force_ssl"
    value = "1"
  }
}
resource "aws_db_instance" "main" {
  identifier                      = var.name
  engine                          = "postgres"
  engine_version                  = var.db_engine_version
  instance_class                  = "db.m6g.large"
  allocated_storage               = 30
  max_allocated_storage           = 100
  storage_type                    = "gp3"
  storage_encrypted               = true
  db_name                         = "orders"
  username                        = "dbowner"
  manage_master_user_password     = true
  multi_az                        = true
  publicly_accessible             = false
  db_subnet_group_name            = aws_db_subnet_group.main.name
  parameter_group_name            = aws_db_parameter_group.main.name
  vpc_security_group_ids          = [aws_security_group.db.id]
  backup_retention_period         = 7
  auto_minor_version_upgrade      = false
  deletion_protection             = var.deletion_protection
  skip_final_snapshot             = false
  final_snapshot_identifier       = var.final_snapshot_id
  enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
}
resource "aws_secretsmanager_secret" "runtime" {
  name                    = "${var.name}/runtime"
  recovery_window_in_days = 7
}
resource "aws_iam_role" "app" {
  name               = "${var.name}-app"
  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" "ssm" {
  role       = aws_iam_role.app.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_role_policy" "runtime" {
  role = aws_iam_role.app.id
  policy = jsonencode({ Version = "2012-10-17", Statement = [
    { Effect = "Allow", Action = "secretsmanager:GetSecretValue", Resource = aws_secretsmanager_secret.runtime.arn },
    { Effect = "Allow", Action = "ecr:GetAuthorizationToken", Resource = "*" },
    { Effect = "Allow", Action = ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:BatchCheckLayerAvailability"], Resource = var.ecr_repository_arn }
  ] })
}
resource "aws_iam_instance_profile" "app" {
  name = var.name
  role = aws_iam_role.app.name
}
resource "aws_instance" "app" {
  for_each                    = local.zones
  ami                         = var.ami_id
  instance_type               = "t3.small"
  subnet_id                   = aws_subnet.app[each.key].id
  vpc_security_group_ids      = [aws_security_group.app.id]
  associate_public_ip_address = false
  iam_instance_profile        = aws_iam_instance_profile.app.name
  user_data_replace_on_change = true
  metadata_options {
    http_tokens                 = "required"
    http_put_response_hop_limit = 1
  }
  root_block_device {
    encrypted   = true
    volume_size = 20
  }
  user_data = templatefile("${path.module}/user-data.sh.tftpl", {
    service = filebase64("${path.module}/three-tier.service")
    deploy  = filebase64("${path.module}/deploy.py")
    config  = base64encode(jsonencode({ region = var.region, image = var.orders_image, secret = aws_secretsmanager_secret.runtime.arn }))
  })
  tags       = { Name = "${var.name}-app-${each.value}" }
  depends_on = [aws_route.nat, aws_route_table_association.app, aws_route_table_association.public, aws_iam_role_policy.runtime, aws_iam_role_policy_attachment.ssm, aws_vpc_security_group_egress_rule.app_https]
}
resource "aws_lb" "main" {
  name                       = var.name
  internal                   = false
  load_balancer_type         = "application"
  subnets                    = [for s in aws_subnet.public : s.id]
  security_groups            = [aws_security_group.alb.id]
  drop_invalid_header_fields = true
  enable_deletion_protection = var.deletion_protection
}
resource "aws_lb_target_group" "app" {
  name                 = var.name
  port                 = 8080
  protocol             = "HTTP"
  vpc_id               = aws_vpc.main.id
  deregistration_delay = 30
  health_check {
    path    = "/readyz"
    matcher = "200"
  }
}
resource "aws_lb_target_group_attachment" "app" {
  for_each         = local.zones
  target_group_arn = aws_lb_target_group.app.arn
  target_id        = aws_instance.app[each.key].id
}
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = var.certificate_arn
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}
output "lab" {
  value = {
    region           = var.region, vpc = aws_vpc.main.id, db_host = aws_db_instance.main.address,
    db_master_secret = aws_db_instance.main.master_user_secret[0].secret_arn,
    runtime_secret   = aws_secretsmanager_secret.runtime.arn,
    instances        = { for az, instance in aws_instance.app : az => instance.id },
    alb_dns          = aws_lb.main.dns_name, target_group = aws_lb_target_group.app.arn,
    data_route_table = aws_route_table.data.id, nat_eips = [for e in aws_eip.nat : e.public_ip]
  }
}
