Files
Claude-Code-Monitor/deployments/terraform/modules/compute/main.tf
T
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

397 lines
13 KiB
Terraform
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ─────────────────────────────────────────────────────────────────────────────
# Compute module Container orchestration with blue/green slot support
#
# Provisions an ECS Fargate service with:
# - Main application container (Express + React)
# - MCP sidecar container
# - EFS persistent volume for SQLite
# - Auto-scaling policies
# - Blue/green deployment slot via variable
# ─────────────────────────────────────────────────────────────────────────────
locals {
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
slot_name = "${local.name_prefix}-${var.deployment_slot}"
common_tags = merge(
{
module = "compute"
deployment_slot = var.deployment_slot
},
var.tags,
)
}
# ─────────────────────────────────────────────────────────────────────────────
# ECS Cluster (shared across slots)
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_ecs_cluster" "main" {
name = "${local.name_prefix}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
configuration {
execute_command_configuration {
logging = "DEFAULT"
}
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-cluster"
})
lifecycle {
prevent_destroy = false
}
}
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy {
base = 1
weight = 1
capacity_provider = "FARGATE"
}
}
# ─────────────────────────────────────────────────────────────────────────────
# IAM roles
# ─────────────────────────────────────────────────────────────────────────────
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
resource "aws_iam_role" "task_execution" {
name = "${local.slot_name}-task-exec"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}]
})
tags = local.common_tags
}
resource "aws_iam_role_policy_attachment" "task_execution" {
role = aws_iam_role.task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role" "task" {
name = "${local.slot_name}-task"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}]
})
tags = local.common_tags
}
# EFS access policy for the task role
resource "aws_iam_role_policy" "task_efs" {
name = "${local.slot_name}-efs-access"
role = aws_iam_role.task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
"elasticfilesystem:ClientRootAccess",
]
Resource = "arn:aws:elasticfilesystem:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:file-system/${var.storage_filesystem_id}"
}]
})
}
# ─────────────────────────────────────────────────────────────────────────────
# CloudWatch log group
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/${local.slot_name}"
retention_in_days = 30
tags = local.common_tags
}
# ─────────────────────────────────────────────────────────────────────────────
# Task definition
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_ecs_task_definition" "app" {
family = local.slot_name
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = tostring(var.cpu)
memory = tostring(var.memory)
execution_role_arn = aws_iam_role.task_execution.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode(concat(
[
{
name = "app"
image = var.container_image
essential = true
cpu = var.mcp_container_image != "" ? floor(var.cpu * 0.75) : var.cpu
memory = var.mcp_container_image != "" ? floor(var.memory * 0.75) : var.memory
portMappings = [
{
containerPort = var.app_port
protocol = "tcp"
}
]
environment = [
for k, v in var.environment_variables : {
name = k
value = v
}
]
mountPoints = [
{
sourceVolume = "app-data"
containerPath = "/app/data"
readOnly = false
}
]
healthCheck = {
command = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${var.app_port}${var.health_check_path} || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = data.aws_region.current.name
"awslogs-stream-prefix" = "app"
}
}
}
],
var.mcp_container_image != "" ? [
{
name = "mcp-sidecar"
image = var.mcp_container_image
essential = false
cpu = floor(var.cpu * 0.25)
memory = floor(var.memory * 0.25)
portMappings = [
{
containerPort = var.mcp_port
protocol = "tcp"
}
]
environment = [
{
name = "NODE_ENV"
value = "production"
},
{
name = "MCP_PORT"
value = tostring(var.mcp_port)
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = data.aws_region.current.name
"awslogs-stream-prefix" = "mcp"
}
}
}
] : []
))
volume {
name = "app-data"
efs_volume_configuration {
file_system_id = var.storage_filesystem_id
root_directory = "/"
transit_encryption = "ENABLED"
authorization_config {
iam = "ENABLED"
}
}
}
tags = local.common_tags
lifecycle {
create_before_destroy = true
}
}
# ─────────────────────────────────────────────────────────────────────────────
# Target group (registered with LB by the loadbalancer module)
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_lb_target_group" "app" {
name_prefix = substr(var.deployment_slot, 0, 5)
port = var.app_port
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
enabled = true
path = var.health_check_path
port = "traffic-port"
protocol = "HTTP"
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 5
interval = 30
matcher = "200"
}
stickiness {
type = "lb_cookie"
cookie_duration = 86400
enabled = true
}
deregistration_delay = 60
tags = merge(local.common_tags, {
Name = "${local.slot_name}-tg"
})
lifecycle {
create_before_destroy = true
}
}
# ─────────────────────────────────────────────────────────────────────────────
# ECS Service
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_ecs_service" "app" {
name = local.slot_name
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.desired_count
launch_type = "FARGATE"
platform_version = "LATEST"
health_check_grace_period_seconds = 120
enable_execute_command = var.environment != "production"
network_configuration {
subnets = var.private_subnet_ids
security_groups = var.security_group_ids
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "app"
container_port = var.app_port
}
deployment_configuration {
maximum_percent = 200
minimum_healthy_percent = 100
}
deployment_circuit_breaker {
enable = true
rollback = true
}
tags = local.common_tags
lifecycle {
ignore_changes = [desired_count]
}
}
# ─────────────────────────────────────────────────────────────────────────────
# Auto-scaling
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_appautoscaling_target" "ecs" {
count = var.max_count > 0 ? 1 : 0
max_capacity = var.max_count
min_capacity = var.min_count
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
tags = local.common_tags
}
resource "aws_appautoscaling_policy" "cpu" {
count = var.max_count > 0 ? 1 : 0
name = "${local.slot_name}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs[0].resource_id
scalable_dimension = aws_appautoscaling_target.ecs[0].scalable_dimension
service_namespace = aws_appautoscaling_target.ecs[0].service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = var.autoscaling_cpu_target
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
resource "aws_appautoscaling_policy" "memory" {
count = var.max_count > 0 ? 1 : 0
name = "${local.slot_name}-memory-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs[0].resource_id
scalable_dimension = aws_appautoscaling_target.ecs[0].scalable_dimension
service_namespace = aws_appautoscaling_target.ecs[0].service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
target_value = var.autoscaling_memory_target
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}