Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions infra/compute/autoscaling.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Target-tracking autoscaling per program service.
resource "aws_appautoscaling_target" "program" {
for_each = toset(var.programs)

service_namespace = "ecs"
resource_id = "service/${aws_ecs_cluster.this.name}/${aws_ecs_service.program[each.value].name}"
scalable_dimension = "ecs:service:DesiredCount"
min_capacity = var.min_capacity
max_capacity = var.max_capacity
}

# CPU utilization target tracking (always on).
resource "aws_appautoscaling_policy" "cpu" {
for_each = aws_appautoscaling_target.program

name = "${local.name_prefix}-${each.key}-cpu"
policy_type = "TargetTrackingScaling"
service_namespace = each.value.service_namespace
resource_id = each.value.resource_id
scalable_dimension = each.value.scalable_dimension

target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}

target_value = var.cpu_target
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}

# Optional queue-depth (backlog per task) target tracking for work-queue driven
# batch services. Enabled when scale_on_queue_depth = true and queue_name set.
resource "aws_appautoscaling_policy" "queue" {
for_each = var.scale_on_queue_depth && var.queue_name != "" ? aws_appautoscaling_target.program : {}

name = "${local.name_prefix}-${each.key}-queue"
policy_type = "TargetTrackingScaling"
service_namespace = each.value.service_namespace
resource_id = each.value.resource_id
scalable_dimension = each.value.scalable_dimension

target_tracking_scaling_policy_configuration {
customized_metric_specification {
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
statistic = "Average"

dimensions {
name = "QueueName"
value = var.queue_name
}
}

target_value = var.queue_backlog_target
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
47 changes: 47 additions & 0 deletions infra/compute/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
data "aws_iam_policy_document" "ecs_assume" {
statement {
actions = ["sts:AssumeRole"]

principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}

# Execution role: pull images from ECR, write logs.
resource "aws_iam_role" "execution" {
name = "${local.name_prefix}-ecs-execution"
assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
tags = { Name = "${local.name_prefix}-ecs-execution" }
}

resource "aws_iam_role_policy_attachment" "execution" {
role = aws_iam_role.execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# Task role: the running container's identity. EFS access via IAM authorization.
resource "aws_iam_role" "task" {
name = "${local.name_prefix}-ecs-task"
assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
tags = { Name = "${local.name_prefix}-ecs-task" }
}

data "aws_iam_policy_document" "task_efs" {
statement {
sid = "EfsClientAccess"
actions = [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
"elasticfilesystem:ClientRootAccess",
]
resources = ["*"]
}
}

resource "aws_iam_role_policy" "task_efs" {
name = "${local.name_prefix}-task-efs"
role = aws_iam_role.task.id
policy = data.aws_iam_policy_document.task_efs.json
}
135 changes: 135 additions & 0 deletions infra/compute/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
locals {
name_prefix = "${var.project}-${var.environment}"

tags = merge(
{
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
Component = "compute"
Application = "CardDemo"
},
var.tags,
)

# Resolve each program's image; placeholder keeps validate/plan working before
# infra/network ECR outputs are wired in.
images = {
for p in var.programs : p => "${lookup(var.ecr_repository_urls, p, "000000000000.dkr.ecr.${var.aws_region}.amazonaws.com/${var.project}/${p}")}:${var.image_tag}"
}
}

resource "aws_ecs_cluster" "this" {
name = "${local.name_prefix}-batch"

setting {
name = "containerInsights"
value = "enabled"
}

tags = { Name = "${local.name_prefix}-batch" }
}

resource "aws_cloudwatch_log_group" "program" {
for_each = toset(var.programs)

name = "/ecs/${local.name_prefix}/${each.value}"
retention_in_days = var.log_retention_days
}

resource "aws_ecs_task_definition" "program" {
for_each = toset(var.programs)

family = "${local.name_prefix}-${each.value}"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.task_cpu
memory = var.task_memory
execution_role_arn = aws_iam_role.execution.arn
task_role_arn = aws_iam_role.task.arn

volume {
name = "carddemo-data"

efs_volume_configuration {
file_system_id = var.efs_file_system_id
transit_encryption = "ENABLED"

authorization_config {
access_point_id = var.efs_access_point_id
iam = "ENABLED"
}
}
}

container_definitions = jsonencode([
{
name = each.value
image = local.images[each.value]
essential = true

environment = [
{ name = "PROGRAM", value = upper(each.value) },
{ name = "DATA_DIR", value = var.data_root },
{ name = "HEALTH_PORT", value = tostring(var.health_port) },
{ name = "MODE", value = "service" },
]

mountPoints = [
{ sourceVolume = "carddemo-data", containerPath = var.data_root, readOnly = false },
]

portMappings = [
{ containerPort = var.health_port, protocol = "tcp" },
]

# In-container liveness probe against the health sidecar (python3 is in the image).
healthCheck = {
command = ["CMD-SHELL", "python3 -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:${var.health_port}/health').status==200 else 1)\" || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 30
}

logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.program[each.value].name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
},
])

runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}

tags = { Name = "${local.name_prefix}-${each.value}" }
}

resource "aws_ecs_service" "program" {
for_each = toset(var.programs)

name = "${local.name_prefix}-${each.value}"
cluster = aws_ecs_cluster.this.id
task_definition = aws_ecs_task_definition.program[each.value].arn
desired_count = var.desired_count
launch_type = "FARGATE"

network_configuration {
subnets = var.private_subnet_ids
security_groups = [var.batch_security_group_id]
assign_public_ip = false
}

deployment_circuit_breaker {
enable = true
rollback = true
}

tags = { Name = "${local.name_prefix}-${each.value}" }
}
29 changes: 29 additions & 0 deletions infra/compute/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
output "cluster_name" {
description = "ECS cluster name."
value = aws_ecs_cluster.this.name
}

output "cluster_arn" {
description = "ECS cluster ARN."
value = aws_ecs_cluster.this.arn
}

output "service_names" {
description = "Map of program -> ECS service name."
value = { for p, s in aws_ecs_service.program : p => s.name }
}

output "task_definition_arns" {
description = "Map of program -> task definition ARN."
value = { for p, t in aws_ecs_task_definition.program : p => t.arn }
}

output "task_role_arn" {
description = "ECS task role ARN."
value = aws_iam_role.task.arn
}

output "execution_role_arn" {
description = "ECS execution role ARN."
value = aws_iam_role.execution.arn
}
Loading