2025-08-05 13:41:43 +00:00
|
|
|
# Phase 2: ECS Fargate + Application Load Balancer
|
|
|
|
|
# File: infrastructure/services/main.tf
|
|
|
|
|
|
|
|
|
|
terraform {
|
|
|
|
|
required_version = ">= 1.0"
|
|
|
|
|
required_providers {
|
|
|
|
|
aws = {
|
|
|
|
|
source = "hashicorp/aws"
|
|
|
|
|
version = "~> 5.0"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
provider "aws" {
|
|
|
|
|
region = var.aws_region
|
|
|
|
|
default_tags {
|
|
|
|
|
tags = var.common_tags
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Data sources to reference Foundation Layer outputs
|
|
|
|
|
data "terraform_remote_state" "foundation" {
|
|
|
|
|
backend = "s3"
|
|
|
|
|
config = {
|
|
|
|
|
bucket = "nvhi-atsila-microservice-terraform-state-c4ae0f80"
|
|
|
|
|
key = "foundation/terraform.tfstate"
|
|
|
|
|
region = var.aws_region
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Enterprise naming convention with AWS resource limits
|
|
|
|
|
locals {
|
|
|
|
|
# Shortened base name for AWS resource naming limits
|
|
|
|
|
base_name = "nvhi-atsila"
|
|
|
|
|
|
|
|
|
|
# Computed names ensuring AWS limits compliance
|
|
|
|
|
# Target Groups (32 char limit): "nvhi-atsila-blue-tg" = 17 chars ✅
|
|
|
|
|
blue_tg_name = "${local.base_name}-blue-tg"
|
|
|
|
|
green_tg_name = "${local.base_name}-green-tg"
|
|
|
|
|
|
|
|
|
|
# ALB name (32 char limit): "nvhi-atsila-alb" = 15 chars ✅
|
|
|
|
|
alb_name = "${local.base_name}-alb"
|
|
|
|
|
|
|
|
|
|
# ECS names for consistency
|
|
|
|
|
ecs_cluster_name = "${local.base_name}-cluster"
|
|
|
|
|
ecs_service_name = "${local.base_name}-service"
|
|
|
|
|
|
|
|
|
|
# ECR name
|
|
|
|
|
ecr_repo_name = "${local.base_name}-app"
|
|
|
|
|
|
|
|
|
|
# Common resource prefix for tagging and organization
|
|
|
|
|
resource_prefix = local.base_name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ECR Repository for container images
|
|
|
|
|
resource "aws_ecr_repository" "app" {
|
|
|
|
|
name = local.ecr_repo_name
|
|
|
|
|
image_tag_mutability = "MUTABLE"
|
|
|
|
|
force_delete = true # For demo purposes
|
|
|
|
|
|
|
|
|
|
image_scanning_configuration {
|
|
|
|
|
scan_on_push = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-ecr"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ECR Lifecycle Policy
|
|
|
|
|
resource "aws_ecr_lifecycle_policy" "app" {
|
|
|
|
|
repository = aws_ecr_repository.app.name
|
|
|
|
|
policy = jsonencode({
|
|
|
|
|
rules = [
|
|
|
|
|
{
|
|
|
|
|
rulePriority = 1
|
|
|
|
|
description = "Keep last 10 images"
|
|
|
|
|
selection = {
|
|
|
|
|
tagStatus = "tagged"
|
|
|
|
|
tagPrefixList = ["v"]
|
|
|
|
|
countType = "imageCountMoreThan"
|
|
|
|
|
countNumber = 10
|
|
|
|
|
}
|
|
|
|
|
action = {
|
|
|
|
|
type = "expire"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
rulePriority = 2
|
|
|
|
|
description = "Delete untagged images older than 1 day"
|
|
|
|
|
selection = {
|
|
|
|
|
tagStatus = "untagged"
|
|
|
|
|
countType = "sinceImagePushed"
|
|
|
|
|
countUnit = "days"
|
|
|
|
|
countNumber = 1
|
|
|
|
|
}
|
|
|
|
|
action = {
|
|
|
|
|
type = "expire"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ECS Cluster
|
|
|
|
|
resource "aws_ecs_cluster" "main" {
|
|
|
|
|
name = local.ecs_cluster_name
|
|
|
|
|
|
|
|
|
|
configuration {
|
|
|
|
|
execute_command_configuration {
|
|
|
|
|
logging = "OVERRIDE"
|
|
|
|
|
log_configuration {
|
|
|
|
|
cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_cluster.name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setting {
|
|
|
|
|
name = "containerInsights"
|
|
|
|
|
value = "enabled"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-ecs-cluster"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# CloudWatch Log Group for ECS
|
|
|
|
|
resource "aws_cloudwatch_log_group" "ecs_cluster" {
|
|
|
|
|
name = "/aws/ecs/${local.ecs_cluster_name}"
|
|
|
|
|
retention_in_days = 7 # Free tier friendly
|
|
|
|
|
skip_destroy = false
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${local.ecs_cluster_name}-logs"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "aws_cloudwatch_log_group" "app" {
|
|
|
|
|
name = "/aws/ecs/${local.ecr_repo_name}"
|
|
|
|
|
retention_in_days = 7 # Free tier friendly
|
|
|
|
|
skip_destroy = false
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${local.ecr_repo_name}-logs"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Application Load Balancer
|
|
|
|
|
resource "aws_lb" "main" {
|
|
|
|
|
name = local.alb_name
|
|
|
|
|
internal = false
|
|
|
|
|
load_balancer_type = "application"
|
|
|
|
|
security_groups = [data.terraform_remote_state.foundation.outputs.alb_security_group_id]
|
|
|
|
|
subnets = data.terraform_remote_state.foundation.outputs.public_subnet_ids
|
|
|
|
|
|
|
|
|
|
enable_deletion_protection = false # For demo purposes
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-alb"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ALB Target Group for Blue Environment
|
|
|
|
|
resource "aws_lb_target_group" "blue" {
|
|
|
|
|
name = local.blue_tg_name
|
|
|
|
|
port = 8080
|
|
|
|
|
protocol = "HTTP"
|
|
|
|
|
vpc_id = data.terraform_remote_state.foundation.outputs.vpc_id
|
|
|
|
|
target_type = "ip"
|
|
|
|
|
|
|
|
|
|
health_check {
|
|
|
|
|
enabled = true
|
|
|
|
|
healthy_threshold = 2
|
|
|
|
|
interval = 30
|
|
|
|
|
matcher = "200"
|
|
|
|
|
path = "/health"
|
|
|
|
|
port = "traffic-port"
|
|
|
|
|
protocol = "HTTP"
|
|
|
|
|
timeout = 5
|
|
|
|
|
unhealthy_threshold = 3
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deregistration_delay = 30 # Fast for demo
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-blue-tg"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ALB Target Group for Green Environment (Blue/Green Deployment)
|
|
|
|
|
resource "aws_lb_target_group" "green" {
|
|
|
|
|
name = local.green_tg_name
|
|
|
|
|
port = 8080
|
|
|
|
|
protocol = "HTTP"
|
|
|
|
|
vpc_id = data.terraform_remote_state.foundation.outputs.vpc_id
|
|
|
|
|
target_type = "ip"
|
|
|
|
|
|
|
|
|
|
health_check {
|
|
|
|
|
enabled = true
|
|
|
|
|
healthy_threshold = 2
|
|
|
|
|
interval = 30
|
|
|
|
|
matcher = "200"
|
|
|
|
|
path = "/health"
|
|
|
|
|
port = "traffic-port"
|
|
|
|
|
protocol = "HTTP"
|
|
|
|
|
timeout = 5
|
|
|
|
|
unhealthy_threshold = 3
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deregistration_delay = 30 # Fast for demo
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-green-tg"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ALB Listener (defaults to Blue)
|
|
|
|
|
resource "aws_lb_listener" "main" {
|
|
|
|
|
load_balancer_arn = aws_lb.main.arn
|
|
|
|
|
port = "80"
|
|
|
|
|
protocol = "HTTP"
|
|
|
|
|
|
|
|
|
|
default_action {
|
|
|
|
|
type = "forward"
|
|
|
|
|
forward {
|
|
|
|
|
target_group {
|
|
|
|
|
arn = aws_lb_target_group.blue.arn
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-alb-listener"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# IAM Role for ECS Task Execution
|
|
|
|
|
resource "aws_iam_role" "ecs_task_execution" {
|
|
|
|
|
name = "${var.project_name}-ecs-task-execution-role"
|
|
|
|
|
|
|
|
|
|
assume_role_policy = jsonencode({
|
|
|
|
|
Version = "2012-10-17"
|
|
|
|
|
Statement = [
|
|
|
|
|
{
|
|
|
|
|
Action = "sts:AssumeRole"
|
|
|
|
|
Effect = "Allow"
|
|
|
|
|
Principal = {
|
|
|
|
|
Service = "ecs-tasks.amazonaws.com"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-ecs-task-execution-role"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# IAM Role Policy Attachment for ECS Task Execution
|
|
|
|
|
resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
|
|
|
|
|
role = aws_iam_role.ecs_task_execution.name
|
|
|
|
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Additional IAM Policy for ECR and CloudWatch
|
|
|
|
|
resource "aws_iam_role_policy" "ecs_task_execution_custom" {
|
|
|
|
|
name = "${var.project_name}-ecs-task-execution-custom"
|
|
|
|
|
role = aws_iam_role.ecs_task_execution.id
|
|
|
|
|
|
|
|
|
|
policy = jsonencode({
|
|
|
|
|
Version = "2012-10-17"
|
|
|
|
|
Statement = [
|
|
|
|
|
{
|
|
|
|
|
Effect = "Allow"
|
|
|
|
|
Action = [
|
|
|
|
|
"ecr:GetAuthorizationToken",
|
|
|
|
|
"ecr:BatchCheckLayerAvailability",
|
|
|
|
|
"ecr:GetDownloadUrlForLayer",
|
|
|
|
|
"ecr:BatchGetImage"
|
|
|
|
|
]
|
|
|
|
|
Resource = "*"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Effect = "Allow"
|
|
|
|
|
Action = [
|
|
|
|
|
"logs:CreateLogStream",
|
|
|
|
|
"logs:PutLogEvents"
|
|
|
|
|
]
|
|
|
|
|
Resource = "${aws_cloudwatch_log_group.app.arn}:*"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# IAM Role for ECS Task (application permissions)
|
|
|
|
|
resource "aws_iam_role" "ecs_task" {
|
|
|
|
|
name = "${var.project_name}-ecs-task-role"
|
|
|
|
|
|
|
|
|
|
assume_role_policy = jsonencode({
|
|
|
|
|
Version = "2012-10-17"
|
|
|
|
|
Statement = [
|
|
|
|
|
{
|
|
|
|
|
Action = "sts:AssumeRole"
|
|
|
|
|
Effect = "Allow"
|
|
|
|
|
Principal = {
|
|
|
|
|
Service = "ecs-tasks.amazonaws.com"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-ecs-task-role"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ECS Task Definition
|
|
|
|
|
resource "aws_ecs_task_definition" "app" {
|
|
|
|
|
family = local.ecr_repo_name
|
|
|
|
|
network_mode = "awsvpc"
|
|
|
|
|
requires_compatibilities = ["FARGATE"]
|
|
|
|
|
cpu = var.task_cpu
|
|
|
|
|
memory = var.task_memory
|
|
|
|
|
execution_role_arn = aws_iam_role.ecs_task_execution.arn
|
|
|
|
|
task_role_arn = aws_iam_role.ecs_task.arn
|
|
|
|
|
|
|
|
|
|
container_definitions = jsonencode([
|
|
|
|
|
{
|
|
|
|
|
name = "app"
|
|
|
|
|
image = "${aws_ecr_repository.app.repository_url}:${var.image_tag}"
|
|
|
|
|
essential = true
|
|
|
|
|
|
|
|
|
|
portMappings = [
|
|
|
|
|
{
|
|
|
|
|
containerPort = 8080
|
|
|
|
|
protocol = "tcp"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
logConfiguration = {
|
|
|
|
|
logDriver = "awslogs"
|
|
|
|
|
options = {
|
|
|
|
|
awslogs-group = aws_cloudwatch_log_group.app.name
|
|
|
|
|
awslogs-region = var.aws_region
|
|
|
|
|
awslogs-stream-prefix = "ecs"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
environment = [
|
|
|
|
|
{
|
|
|
|
|
name = "ENV"
|
|
|
|
|
value = var.environment
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name = "PROJECT_NAME"
|
|
|
|
|
value = var.project_name
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-task-definition"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ECS Service (Blue Environment)
|
|
|
|
|
resource "aws_ecs_service" "app" {
|
|
|
|
|
name = local.ecs_service_name
|
|
|
|
|
cluster = aws_ecs_cluster.main.id
|
|
|
|
|
task_definition = aws_ecs_task_definition.app.arn
|
|
|
|
|
desired_count = var.desired_count
|
|
|
|
|
launch_type = "FARGATE"
|
|
|
|
|
|
|
|
|
|
network_configuration {
|
|
|
|
|
subnets = data.terraform_remote_state.foundation.outputs.app_subnet_ids
|
|
|
|
|
security_groups = [data.terraform_remote_state.foundation.outputs.ecs_tasks_security_group_id]
|
|
|
|
|
assign_public_ip = true # Required for public subnets without NAT gateway
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
load_balancer {
|
|
|
|
|
target_group_arn = aws_lb_target_group.blue.arn
|
|
|
|
|
container_name = "app"
|
|
|
|
|
container_port = 8080
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
depends_on = [aws_lb_listener.main]
|
|
|
|
|
|
|
|
|
|
# Blue/Green deployment configuration
|
2025-08-06 04:54:40 +00:00
|
|
|
deployment_maximum_percent = 200
|
|
|
|
|
deployment_minimum_healthy_percent = 50
|
2025-08-06 04:42:15 +00:00
|
|
|
|
|
|
|
|
deployment_circuit_breaker {
|
|
|
|
|
enable = true
|
|
|
|
|
rollback = true
|
2025-08-05 13:41:43 +00:00
|
|
|
}
|
|
|
|
|
|
2025-08-06 04:42:15 +00:00
|
|
|
|
2025-08-05 13:41:43 +00:00
|
|
|
enable_execute_command = true
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-ecs-service"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# CloudWatch Alarms for monitoring
|
|
|
|
|
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
|
|
|
|
|
alarm_name = "${var.project_name}-high-cpu"
|
|
|
|
|
comparison_operator = "GreaterThanThreshold"
|
|
|
|
|
evaluation_periods = "2"
|
|
|
|
|
metric_name = "CPUUtilization"
|
|
|
|
|
namespace = "AWS/ECS"
|
|
|
|
|
period = "300"
|
|
|
|
|
statistic = "Average"
|
|
|
|
|
threshold = "80"
|
|
|
|
|
alarm_description = "This metric monitors ECS service CPU utilization"
|
|
|
|
|
alarm_actions = [] # Add SNS topic ARN for notifications
|
|
|
|
|
|
|
|
|
|
dimensions = {
|
|
|
|
|
ServiceName = aws_ecs_service.app.name
|
|
|
|
|
ClusterName = aws_ecs_cluster.main.name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-high-cpu-alarm"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "aws_cloudwatch_metric_alarm" "high_memory" {
|
|
|
|
|
alarm_name = "${var.project_name}-high-memory"
|
|
|
|
|
comparison_operator = "GreaterThanThreshold"
|
|
|
|
|
evaluation_periods = "2"
|
|
|
|
|
metric_name = "MemoryUtilization"
|
|
|
|
|
namespace = "AWS/ECS"
|
|
|
|
|
period = "300"
|
|
|
|
|
statistic = "Average"
|
|
|
|
|
threshold = "80"
|
|
|
|
|
alarm_description = "This metric monitors ECS service memory utilization"
|
|
|
|
|
alarm_actions = [] # Add SNS topic ARN for notifications
|
|
|
|
|
|
|
|
|
|
dimensions = {
|
|
|
|
|
ServiceName = aws_ecs_service.app.name
|
|
|
|
|
ClusterName = aws_ecs_cluster.main.name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tags = {
|
|
|
|
|
Name = "${var.project_name}-high-memory-alarm"
|
|
|
|
|
Environment = var.environment
|
|
|
|
|
Project = var.project_name
|
|
|
|
|
}
|
|
|
|
|
}
|