# Using the module
module "api_service" {
source = "./modules/ecs_service"
service_name = "api"
cluster_id = aws_ecs_cluster.main.id
cluster_name = aws_ecs_cluster.main.name
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
image_repository = "123456789.dkr.ecr.us-east-1.amazonaws.com/api"
image_tag = var.api_image_tag
container_port = 3000
health_check_path = "/health"
cpu = 512
memory = 1024
desired_count = 3
min_count = 2
max_count = 10
aws_region = var.aws_region
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.api_task.arn
alb_security_group_id = aws_security_group.alb.id
log_retention_days = 30
environment_variables = {
NODE_ENV = "production"
REDIS_URL = aws_elasticache_cluster.main.cache_nodes[0].address
}
secrets = {
DATABASE_URL = aws_secretsmanager_secret.db_url.arn
JWT_SECRET = aws_secretsmanager_secret.jwt.arn
}
}
module "worker_service" {
source = "./modules/ecs_service"
service_name = "worker"
cluster_id = aws_ecs_cluster.main.id
cluster_name = aws_ecs_cluster.main.name
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
image_repository = "123456789.dkr.ecr.us-east-1.amazonaws.com/worker"
image_tag = var.worker_image_tag
container_port = 8080
health_check_path = "/health"
cpu = 1024
memory = 2048
desired_count = 2
min_count = 1
max_count = 8
aws_region = var.aws_region
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.worker_task.arn
alb_security_group_id = aws_security_group.alb.id
log_retention_days = 14
environment_variables = {
NODE_ENV = "production"
QUEUE_NAME = "default"
CONCURRENCY = "5"
}
secrets = {
DATABASE_URL = aws_secretsmanager_secret.db_url.arn
API_KEY = aws_secretsmanager_secret.api_key.arn
}
}
# Reusable ECS Service module
resource "aws_ecs_task_definition" "this" {
family = var.service_name
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.cpu
memory = var.memory
execution_role_arn = var.execution_role_arn
task_role_arn = var.task_role_arn
container_definitions = jsonencode([
{
name = var.service_name
image = "${var.image_repository}:${var.image_tag}"
essential = true
portMappings = [
{
containerPort = var.container_port
protocol = "tcp"
}
]
environment = [
for key, value in var.environment_variables : {
name = key
value = value
}
]
secrets = [
for key, arn in var.secrets : {
name = key
valueFrom = arn
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.this.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = var.service_name
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:${var.container_port}${var.health_check_path} || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
}
resource "aws_ecs_service" "this" {
name = var.service_name
cluster = var.cluster_id
task_definition = aws_ecs_task_definition.this.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.subnet_ids
security_groups = [aws_security_group.this.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.this.arn
container_name = var.service_name
container_port = var.container_port
}
deployment_circuit_breaker {
enable = true
rollback = true
}
lifecycle {
ignore_changes = [desired_count]
}
}
resource "aws_appautoscaling_target" "this" {
max_capacity = var.max_count
min_capacity = var.min_count
resource_id = "service/${var.cluster_name}/${aws_ecs_service.this.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
name = "${var.service_name}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.this.resource_id
scalable_dimension = aws_appautoscaling_target.this.scalable_dimension
service_namespace = aws_appautoscaling_target.this.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0
}
}
resource "aws_cloudwatch_log_group" "this" {
name = "/ecs/${var.service_name}"
retention_in_days = var.log_retention_days
}
resource "aws_security_group" "this" {
name_prefix = "${var.service_name}-"
vpc_id = var.vpc_id
ingress {
from_port = var.container_port
to_port = var.container_port
protocol = "tcp"
security_groups = [var.alb_security_group_id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_lb_target_group" "this" {
name = var.service_name
port = var.container_port
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
path = var.health_check_path
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 5
interval = 30
}
}
Terraform modules encapsulate related resources into reusable, composable packages. A module is simply a directory with .tf files. The root module calls child modules with the module block. Input variable blocks parameterize modules. output blocks expose values to the caller. Modules can be sourced from local paths, Git repositories, or the Terraform Registry. Version constraints pin module versions. Module composition builds complex infrastructure from simple building blocks. for_each and count create multiple module instances. locals compute intermediate values. Well-designed modules have clear interfaces, sensible defaults, and comprehensive documentation. The DRY principle applies—extract common patterns into shared modules.