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.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit 57dc91585d
783 changed files with 221743 additions and 0 deletions
@@ -0,0 +1,396 @@
# ─────────────────────────────────────────────────────────────────────────────
# 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
}
}
@@ -0,0 +1,53 @@
# ─────────────────────────────────────────────────────────────────────────────
# Compute module outputs
# ─────────────────────────────────────────────────────────────────────────────
output "cluster_name" {
description = "Name of the ECS cluster"
value = aws_ecs_cluster.main.name
}
output "cluster_arn" {
description = "ARN of the ECS cluster"
value = aws_ecs_cluster.main.arn
}
output "service_name" {
description = "Name of the ECS service for this slot"
value = aws_ecs_service.app.name
}
output "service_arn" {
description = "ARN of the ECS service for this slot"
value = aws_ecs_service.app.id
}
output "task_definition_arn" {
description = "ARN of the current task definition"
value = aws_ecs_task_definition.app.arn
}
output "target_group_arn" {
description = "ARN of the target group for LB registration"
value = aws_lb_target_group.app.arn
}
output "target_group_name" {
description = "Name of the target group"
value = aws_lb_target_group.app.name
}
output "log_group_name" {
description = "CloudWatch log group name"
value = aws_cloudwatch_log_group.app.name
}
output "task_execution_role_arn" {
description = "ARN of the task execution IAM role"
value = aws_iam_role.task_execution.arn
}
output "task_role_arn" {
description = "ARN of the task IAM role"
value = aws_iam_role.task.arn
}
@@ -0,0 +1,146 @@
# ─────────────────────────────────────────────────────────────────────────────
# Compute module variables
# ─────────────────────────────────────────────────────────────────────────────
variable "project_name" {
description = "Project identifier used in resource naming"
type = string
}
variable "environment" {
description = "Deployment environment (dev, staging, production)"
type = string
}
variable "cloud_provider" {
description = "Target cloud provider (aws, gcp, azure, oci)"
type = string
}
variable "region" {
description = "Cloud region for deployment"
type = string
}
variable "deployment_slot" {
description = "Deployment slot identifier for blue-green: blue or green"
type = string
default = "blue"
validation {
condition = contains(["blue", "green"], var.deployment_slot)
error_message = "deployment_slot must be blue or green."
}
}
variable "container_image" {
description = "Container image URI for the main application"
type = string
}
variable "mcp_container_image" {
description = "Container image URI for the MCP sidecar (empty to disable)"
type = string
default = ""
}
variable "app_port" {
description = "Port the application container listens on"
type = number
default = 4820
}
variable "mcp_port" {
description = "Port the MCP sidecar container listens on"
type = number
default = 8819
}
variable "cpu" {
description = "CPU units for the task (256, 512, 1024, 2048, 4096)"
type = number
default = 512
validation {
condition = contains([256, 512, 1024, 2048, 4096], var.cpu)
error_message = "cpu must be one of: 256, 512, 1024, 2048, 4096."
}
}
variable "memory" {
description = "Memory in MiB for the task"
type = number
default = 1024
}
variable "desired_count" {
description = "Desired number of running task instances"
type = number
default = 1
}
variable "min_count" {
description = "Minimum number of task instances for auto-scaling"
type = number
default = 1
}
variable "max_count" {
description = "Maximum number of task instances for auto-scaling"
type = number
default = 3
}
variable "environment_variables" {
description = "Map of environment variables for the application container"
type = map(string)
default = {}
}
variable "health_check_path" {
description = "HTTP path for container health checks"
type = string
default = "/api/health"
}
variable "vpc_id" {
description = "VPC ID for target group and networking"
type = string
}
variable "private_subnet_ids" {
description = "Subnet IDs where tasks will be placed"
type = list(string)
}
variable "security_group_ids" {
description = "Security group IDs attached to task ENIs"
type = list(string)
}
variable "storage_filesystem_id" {
description = "EFS file system ID for persistent SQLite storage"
type = string
}
variable "storage_mount_targets" {
description = "EFS mount target IDs (ensures mount targets exist before service)"
type = list(string)
default = []
}
variable "autoscaling_cpu_target" {
description = "Target CPU utilization percentage for auto-scaling"
type = number
default = 70
}
variable "autoscaling_memory_target" {
description = "Target memory utilization percentage for auto-scaling"
type = number
default = 80
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}
@@ -0,0 +1,142 @@
# ─────────────────────────────────────────────────────────────────────────────
# Database module Persistent storage for SQLite
#
# Creates a managed network file system (EFS on AWS) with:
# - Encryption at rest and in transit
# - Automated backup policy
# - Mount targets in each private subnet
# - Performance mode optimised for SQLite workloads
# ─────────────────────────────────────────────────────────────────────────────
locals {
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
common_tags = merge(
{
module = "database"
},
var.tags,
)
}
# ─────────────────────────────────────────────────────────────────────────────
# EFS file system
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_efs_file_system" "main" {
creation_token = "${local.name_prefix}-data"
encrypted = true
# General Purpose is optimal for SQLite (latency-sensitive small I/O)
performance_mode = "generalPurpose"
throughput_mode = "elastic"
lifecycle_policy {
transition_to_ia = "AFTER_30_DAYS"
}
lifecycle_policy {
transition_to_primary_storage_class = "AFTER_1_ACCESS"
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-efs"
})
lifecycle {
prevent_destroy = true
}
}
# ─────────────────────────────────────────────────────────────────────────────
# EFS backup policy
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_efs_backup_policy" "main" {
file_system_id = aws_efs_file_system.main.id
backup_policy {
status = var.enable_backup ? "ENABLED" : "DISABLED"
}
}
# ─────────────────────────────────────────────────────────────────────────────
# EFS mount targets (one per private subnet / AZ)
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_efs_mount_target" "main" {
count = length(var.private_subnet_ids)
file_system_id = aws_efs_file_system.main.id
subnet_id = var.private_subnet_ids[count.index]
security_groups = var.allowed_security_group_ids
}
# ─────────────────────────────────────────────────────────────────────────────
# EFS access point scoped to /app/data for the container workload
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_efs_access_point" "app_data" {
file_system_id = aws_efs_file_system.main.id
posix_user {
uid = 1000
gid = 1000
}
root_directory {
path = "/app-data"
creation_info {
owner_uid = 1000
owner_gid = 1000
permissions = "0755"
}
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-app-data-ap"
})
}
# ─────────────────────────────────────────────────────────────────────────────
# EFS file system policy enforce encryption in transit
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_efs_file_system_policy" "main" {
file_system_id = aws_efs_file_system.main.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EnforceEncryptInTransit"
Effect = "Deny"
Principal = { AWS = "*" }
Action = "*"
Resource = aws_efs_file_system.main.arn
Condition = {
Bool = {
"aws:SecureTransport" = "false"
}
}
},
{
Sid = "AllowMountViaAccessPoint"
Effect = "Allow"
Principal = { AWS = "*" }
Action = [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
"elasticfilesystem:ClientRootAccess",
]
Resource = aws_efs_file_system.main.arn
Condition = {
Bool = {
"elasticfilesystem:AccessedViaMountTarget" = "true"
}
}
}
]
})
}
@@ -0,0 +1,38 @@
# ─────────────────────────────────────────────────────────────────────────────
# Database module outputs
# ─────────────────────────────────────────────────────────────────────────────
output "filesystem_id" {
description = "ID of the EFS file system"
value = aws_efs_file_system.main.id
}
output "filesystem_arn" {
description = "ARN of the EFS file system"
value = aws_efs_file_system.main.arn
}
output "filesystem_dns_name" {
description = "DNS name of the EFS file system"
value = aws_efs_file_system.main.dns_name
}
output "mount_target_ids" {
description = "IDs of the EFS mount targets"
value = aws_efs_mount_target.main[*].id
}
output "mount_target_ips" {
description = "IP addresses of the EFS mount targets"
value = aws_efs_mount_target.main[*].ip_address
}
output "access_point_id" {
description = "ID of the EFS access point for /app/data"
value = aws_efs_access_point.app_data.id
}
output "access_point_arn" {
description = "ARN of the EFS access point"
value = aws_efs_access_point.app_data.arn
}
@@ -0,0 +1,60 @@
# ─────────────────────────────────────────────────────────────────────────────
# Database module variables
# ─────────────────────────────────────────────────────────────────────────────
variable "project_name" {
description = "Project identifier used in resource naming"
type = string
}
variable "environment" {
description = "Deployment environment (dev, staging, production)"
type = string
}
variable "cloud_provider" {
description = "Target cloud provider (aws, gcp, azure, oci)"
type = string
}
variable "region" {
description = "Cloud region for deployment"
type = string
}
variable "storage_size_gb" {
description = "Storage allocation in GiB (used by providers with provisioned capacity)"
type = number
default = 20
validation {
condition = var.storage_size_gb >= 1
error_message = "storage_size_gb must be at least 1 GiB."
}
}
variable "enable_backup" {
description = "Enable automated backup of the file system"
type = bool
default = true
}
variable "private_subnet_ids" {
description = "Private subnet IDs for mount targets"
type = list(string)
}
variable "vpc_id" {
description = "VPC ID for security group association"
type = string
}
variable "allowed_security_group_ids" {
description = "Security group IDs allowed to mount the file system"
type = list(string)
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}
@@ -0,0 +1,247 @@
# ─────────────────────────────────────────────────────────────────────────────
# Load Balancer module Application load balancer with WebSocket support
#
# Provisions:
# - ALB in public subnets
# - HTTPS listener with TLS termination
# - HTTP → HTTPS redirect
# - Weighted target groups for blue/green and canary deployments
# - Sticky sessions for WebSocket connections
# - Path-based routing for MCP sidecar (/mcp/*)
# - Health checks at /api/health
# ─────────────────────────────────────────────────────────────────────────────
# Production TLS enforcement — prevents deploying production without encryption
check "production_tls_required" {
assert {
condition = var.environment != "production" || var.tls_certificate_arn != ""
error_message = "Production deployments require TLS. Set tls_certificate_arn and domain_name."
}
}
check "production_domain_required" {
assert {
condition = var.environment != "production" || var.domain_name != ""
error_message = "Production deployments require a domain name. Set domain_name."
}
}
locals {
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
# Determine whether TLS is configured
has_tls = var.tls_certificate_arn != ""
common_tags = merge(
{
module = "loadbalancer"
},
var.tags,
)
}
# ─────────────────────────────────────────────────────────────────────────────
# Application Load Balancer
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_lb" "main" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = var.security_group_ids
subnets = var.public_subnet_ids
enable_deletion_protection = var.environment == "production" ? true : var.enable_deletion_protection
enable_http2 = true
idle_timeout = 300 # WebSocket connections may be long-lived
drop_invalid_header_fields = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-alb"
})
lifecycle {
prevent_destroy = false
# For production, set enable_deletion_protection = true above (enforced automatically)
}
}
# ─────────────────────────────────────────────────────────────────────────────
# HTTPS listener (primary with weighted target groups for blue/green)
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_lb_listener" "https" {
count = local.has_tls ? 1 : 0
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.tls_certificate_arn
default_action {
type = "forward"
forward {
target_group {
arn = var.blue_target_group_arn
weight = var.blue_weight
}
target_group {
arn = var.green_target_group_arn
weight = var.green_weight
}
stickiness {
enabled = true
duration = 86400
}
}
}
tags = local.common_tags
}
# ─────────────────────────────────────────────────────────────────────────────
# HTTP listener redirect to HTTPS when TLS is configured, else forward
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_lb_listener" "http_redirect" {
count = local.has_tls ? 1 : 0
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
tags = local.common_tags
}
resource "aws_lb_listener" "http_forward" {
count = local.has_tls ? 0 : 1
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
forward {
target_group {
arn = var.blue_target_group_arn
weight = var.blue_weight
}
target_group {
arn = var.green_target_group_arn
weight = var.green_weight
}
stickiness {
enabled = true
duration = 86400
}
}
}
tags = local.common_tags
}
# ─────────────────────────────────────────────────────────────────────────────
# MCP sidecar target group
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_lb_target_group" "mcp" {
name_prefix = "mcp-"
port = var.mcp_port
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
enabled = true
path = "/"
port = tostring(var.mcp_port)
protocol = "HTTP"
healthy_threshold = var.health_check_healthy_threshold
unhealthy_threshold = var.health_check_unhealthy_threshold
timeout = var.health_check_timeout
interval = var.health_check_interval
matcher = "200-404"
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-mcp-tg"
})
lifecycle {
create_before_destroy = true
}
}
# ─────────────────────────────────────────────────────────────────────────────
# Path-based routing rule for MCP sidecar (/mcp/*)
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_lb_listener_rule" "mcp_https" {
count = local.has_tls ? 1 : 0
listener_arn = aws_lb_listener.https[0].arn
priority = 10
action {
type = "forward"
target_group_arn = aws_lb_target_group.mcp.arn
}
condition {
path_pattern {
values = ["/mcp", "/mcp/*"]
}
}
tags = local.common_tags
}
resource "aws_lb_listener_rule" "mcp_http" {
count = local.has_tls ? 0 : 1
listener_arn = aws_lb_listener.http_forward[0].arn
priority = 10
action {
type = "forward"
target_group_arn = aws_lb_target_group.mcp.arn
}
condition {
path_pattern {
values = ["/mcp", "/mcp/*"]
}
}
tags = local.common_tags
}
# ─────────────────────────────────────────────────────────────────────────────
# Weight sum validation
# ─────────────────────────────────────────────────────────────────────────────
check "lb_weight_sum" {
assert {
condition = var.blue_weight + var.green_weight == 100
error_message = "blue_weight (${var.blue_weight}) + green_weight (${var.green_weight}) must sum to 100 for correct traffic routing."
}
}
@@ -0,0 +1,43 @@
# ─────────────────────────────────────────────────────────────────────────────
# Load Balancer module outputs
# ─────────────────────────────────────────────────────────────────────────────
output "loadbalancer_arn" {
description = "ARN of the application load balancer"
value = aws_lb.main.arn
}
output "loadbalancer_id" {
description = "ID of the application load balancer"
value = aws_lb.main.id
}
output "dns_name" {
description = "DNS name of the application load balancer"
value = aws_lb.main.dns_name
}
output "zone_id" {
description = "Route53 zone ID for the load balancer (alias records)"
value = aws_lb.main.zone_id
}
output "application_url" {
description = "Full URL to access the application"
value = local.has_tls ? "https://${var.domain_name != "" ? var.domain_name : aws_lb.main.dns_name}" : "http://${aws_lb.main.dns_name}"
}
output "https_listener_arn" {
description = "ARN of the HTTPS listener (empty if TLS not configured)"
value = local.has_tls ? aws_lb_listener.https[0].arn : ""
}
output "http_listener_arn" {
description = "ARN of the HTTP listener"
value = local.has_tls ? aws_lb_listener.http_redirect[0].arn : aws_lb_listener.http_forward[0].arn
}
output "mcp_target_group_arn" {
description = "ARN of the MCP sidecar target group"
value = aws_lb_target_group.mcp.arn
}
@@ -0,0 +1,134 @@
# ─────────────────────────────────────────────────────────────────────────────
# Load Balancer module variables
# ─────────────────────────────────────────────────────────────────────────────
variable "project_name" {
description = "Project identifier used in resource naming"
type = string
}
variable "environment" {
description = "Deployment environment (dev, staging, production)"
type = string
}
variable "cloud_provider" {
description = "Target cloud provider (aws, gcp, azure, oci)"
type = string
}
variable "region" {
description = "Cloud region for deployment"
type = string
}
variable "vpc_id" {
description = "VPC ID for target group association"
type = string
}
variable "public_subnet_ids" {
description = "Public subnet IDs for load balancer placement"
type = list(string)
}
variable "security_group_ids" {
description = "Security group IDs attached to the load balancer"
type = list(string)
}
variable "app_port" {
description = "Application container port"
type = number
default = 4820
}
variable "mcp_port" {
description = "MCP sidecar container port"
type = number
default = 8819
}
variable "tls_certificate_arn" {
description = "ARN of the TLS certificate for HTTPS (required for production)"
type = string
default = ""
}
variable "domain_name" {
description = "Fully qualified domain name for the application (required for production)"
type = string
default = ""
}
variable "blue_target_group_arn" {
description = "ARN of the blue deployment target group"
type = string
}
variable "green_target_group_arn" {
description = "ARN of the green deployment target group"
type = string
}
variable "blue_weight" {
description = "Traffic weight for blue target group (0-100)"
type = number
default = 100
validation {
condition = var.blue_weight >= 0 && var.blue_weight <= 100
error_message = "blue_weight must be between 0 and 100."
}
}
variable "green_weight" {
description = "Traffic weight for green target group (0-100)"
type = number
default = 0
validation {
condition = var.green_weight >= 0 && var.green_weight <= 100
error_message = "green_weight must be between 0 and 100."
}
}
variable "health_check_path" {
description = "HTTP path for health checks"
type = string
default = "/api/health"
}
variable "health_check_interval" {
description = "Seconds between health checks"
type = number
default = 30
}
variable "health_check_timeout" {
description = "Seconds before a health check times out"
type = number
default = 5
}
variable "health_check_healthy_threshold" {
description = "Consecutive successes to mark healthy"
type = number
default = 2
}
variable "health_check_unhealthy_threshold" {
description = "Consecutive failures to mark unhealthy"
type = number
default = 3
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}
variable "enable_deletion_protection" {
description = "Enable deletion protection on the load balancer (recommended for production)"
type = bool
default = false
}
@@ -0,0 +1,312 @@
# ─────────────────────────────────────────────────────────────────────────────
# Monitoring module Observability, alerting, and dashboards
#
# Provisions:
# - CloudWatch log groups for centralized log aggregation
# - Metric alarms for error rate, latency, disk, unhealthy hosts
# - SNS topic for alert notifications
# - CloudWatch dashboard with key operational metrics
# ─────────────────────────────────────────────────────────────────────────────
locals {
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
common_tags = merge(
{
module = "monitoring"
},
var.tags,
)
# Parse ALB ARN suffix for CloudWatch metric dimensions
alb_arn_suffix = try(
regex("app/.*$", var.loadbalancer_arn),
""
)
}
# ─────────────────────────────────────────────────────────────────────────────
# SNS topic for alert notifications
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_sns_topic" "alerts" {
name = "${local.name_prefix}-alerts"
tags = local.common_tags
}
resource "aws_sns_topic_subscription" "email" {
count = var.alert_email != "" ? 1 : 0
topic_arn = aws_sns_topic.alerts.arn
protocol = "email"
endpoint = var.alert_email
}
# ─────────────────────────────────────────────────────────────────────────────
# CloudWatch log group (application-level)
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_cloudwatch_log_group" "application" {
name = "/ccam/${local.name_prefix}"
retention_in_days = var.log_retention_days
tags = local.common_tags
}
# ─────────────────────────────────────────────────────────────────────────────
# Metric alarms
# ─────────────────────────────────────────────────────────────────────────────
# High 5xx error rate from ALB
resource "aws_cloudwatch_metric_alarm" "high_5xx_rate" {
alarm_name = "${local.name_prefix}-high-5xx-error-rate"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "HTTPCode_Target_5XX_Count"
namespace = "AWS/ApplicationELB"
period = 60
statistic = "Sum"
threshold = 10
alarm_description = "High 5XX error rate detected on ${local.name_prefix} ALB"
treat_missing_data = "notBreaching"
dimensions = {
LoadBalancer = local.alb_arn_suffix
}
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
tags = local.common_tags
}
# High target response time (latency)
resource "aws_cloudwatch_metric_alarm" "high_latency" {
alarm_name = "${local.name_prefix}-high-latency"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "TargetResponseTime"
namespace = "AWS/ApplicationELB"
period = 60
statistic = "Average"
threshold = 2.0 # seconds
alarm_description = "High average latency (>2s) on ${local.name_prefix} ALB"
treat_missing_data = "notBreaching"
dimensions = {
LoadBalancer = local.alb_arn_suffix
}
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
tags = local.common_tags
}
# Unhealthy host count
resource "aws_cloudwatch_metric_alarm" "unhealthy_hosts" {
count = length(var.target_group_arns)
alarm_name = "${local.name_prefix}-unhealthy-hosts-${count.index}"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "UnHealthyHostCount"
namespace = "AWS/ApplicationELB"
period = 60
statistic = "Maximum"
threshold = 0
alarm_description = "Unhealthy targets detected in target group ${count.index}"
treat_missing_data = "notBreaching"
dimensions = {
LoadBalancer = local.alb_arn_suffix
TargetGroup = try(regex("targetgroup/.*$", var.target_group_arns[count.index]), "")
}
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
tags = local.common_tags
}
# EFS burst credit balance (low disk throughput)
resource "aws_cloudwatch_metric_alarm" "efs_burst_credits" {
alarm_name = "${local.name_prefix}-efs-low-burst-credits"
comparison_operator = "LessThanThreshold"
evaluation_periods = 3
metric_name = "BurstCreditBalance"
namespace = "AWS/EFS"
period = 300
statistic = "Average"
threshold = 1000000000 # 1 GiB in bytes
alarm_description = "EFS burst credits running low for ${local.name_prefix}"
treat_missing_data = "notBreaching"
dimensions = {
FileSystemId = var.filesystem_id
}
alarm_actions = [aws_sns_topic.alerts.arn]
tags = local.common_tags
}
# ECS CPU utilisation (cluster-level)
resource "aws_cloudwatch_metric_alarm" "ecs_high_cpu" {
alarm_name = "${local.name_prefix}-ecs-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
period = 300
statistic = "Average"
threshold = 85
alarm_description = "High ECS CPU utilisation (>85%) for cluster ${var.compute_cluster_name}"
treat_missing_data = "notBreaching"
dimensions = {
ClusterName = var.compute_cluster_name
}
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
tags = local.common_tags
}
# ECS Memory utilisation
resource "aws_cloudwatch_metric_alarm" "ecs_high_memory" {
alarm_name = "${local.name_prefix}-ecs-high-memory"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "MemoryUtilization"
namespace = "AWS/ECS"
period = 300
statistic = "Average"
threshold = 85
alarm_description = "High ECS memory utilisation (>85%) for cluster ${var.compute_cluster_name}"
treat_missing_data = "notBreaching"
dimensions = {
ClusterName = var.compute_cluster_name
}
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
tags = local.common_tags
}
# ─────────────────────────────────────────────────────────────────────────────
# CloudWatch Dashboard
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_cloudwatch_dashboard" "main" {
dashboard_name = local.name_prefix
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
x = 0
y = 0
width = 12
height = 6
properties = {
title = "ALB Request Count"
region = var.region
metrics = [
["AWS/ApplicationELB", "RequestCount", "LoadBalancer", local.alb_arn_suffix, { stat = "Sum", period = 60 }]
]
view = "timeSeries"
stacked = false
}
},
{
type = "metric"
x = 12
y = 0
width = 12
height = 6
properties = {
title = "ALB Response Time"
region = var.region
metrics = [
["AWS/ApplicationELB", "TargetResponseTime", "LoadBalancer", local.alb_arn_suffix, { stat = "Average", period = 60 }],
["AWS/ApplicationELB", "TargetResponseTime", "LoadBalancer", local.alb_arn_suffix, { stat = "p99", period = 60 }],
]
view = "timeSeries"
stacked = false
}
},
{
type = "metric"
x = 0
y = 6
width = 12
height = 6
properties = {
title = "HTTP Error Rates"
region = var.region
metrics = [
["AWS/ApplicationELB", "HTTPCode_Target_4XX_Count", "LoadBalancer", local.alb_arn_suffix, { stat = "Sum", period = 60 }],
["AWS/ApplicationELB", "HTTPCode_Target_5XX_Count", "LoadBalancer", local.alb_arn_suffix, { stat = "Sum", period = 60 }],
]
view = "timeSeries"
stacked = false
}
},
{
type = "metric"
x = 12
y = 6
width = 12
height = 6
properties = {
title = "ECS CPU & Memory"
region = var.region
metrics = [
["AWS/ECS", "CPUUtilization", "ClusterName", var.compute_cluster_name, { stat = "Average", period = 60 }],
["AWS/ECS", "MemoryUtilization", "ClusterName", var.compute_cluster_name, { stat = "Average", period = 60 }],
]
view = "timeSeries"
stacked = false
}
},
{
type = "metric"
x = 0
y = 12
width = 12
height = 6
properties = {
title = "EFS I/O"
region = var.region
metrics = [
["AWS/EFS", "DataReadIOBytes", "FileSystemId", var.filesystem_id, { stat = "Sum", period = 60 }],
["AWS/EFS", "DataWriteIOBytes", "FileSystemId", var.filesystem_id, { stat = "Sum", period = 60 }],
]
view = "timeSeries"
stacked = false
}
},
{
type = "metric"
x = 12
y = 12
width = 12
height = 6
properties = {
title = "Healthy vs Unhealthy Hosts"
region = var.region
metrics = [
["AWS/ApplicationELB", "HealthyHostCount", "LoadBalancer", local.alb_arn_suffix, { stat = "Average", period = 60 }],
["AWS/ApplicationELB", "UnHealthyHostCount", "LoadBalancer", local.alb_arn_suffix, { stat = "Average", period = 60 }],
]
view = "timeSeries"
stacked = false
}
},
]
})
}
@@ -0,0 +1,40 @@
# ─────────────────────────────────────────────────────────────────────────────
# Monitoring module outputs
# ─────────────────────────────────────────────────────────────────────────────
output "sns_topic_arn" {
description = "ARN of the SNS alert topic"
value = aws_sns_topic.alerts.arn
}
output "log_group_name" {
description = "Name of the CloudWatch log group"
value = aws_cloudwatch_log_group.application.name
}
output "log_group_arn" {
description = "ARN of the CloudWatch log group"
value = aws_cloudwatch_log_group.application.arn
}
output "dashboard_name" {
description = "Name of the CloudWatch dashboard"
value = aws_cloudwatch_dashboard.main.dashboard_name
}
output "dashboard_url" {
description = "URL to the CloudWatch dashboard in the AWS console"
value = "https://${var.region}.console.aws.amazon.com/cloudwatch/home?region=${var.region}#dashboards:name=${aws_cloudwatch_dashboard.main.dashboard_name}"
}
output "alarm_arns" {
description = "ARNs of all configured CloudWatch alarms"
value = concat(
[aws_cloudwatch_metric_alarm.high_5xx_rate.arn],
[aws_cloudwatch_metric_alarm.high_latency.arn],
[aws_cloudwatch_metric_alarm.efs_burst_credits.arn],
[aws_cloudwatch_metric_alarm.ecs_high_cpu.arn],
[aws_cloudwatch_metric_alarm.ecs_high_memory.arn],
aws_cloudwatch_metric_alarm.unhealthy_hosts[*].arn,
)
}
@@ -0,0 +1,66 @@
# ─────────────────────────────────────────────────────────────────────────────
# Monitoring module variables
# ─────────────────────────────────────────────────────────────────────────────
variable "project_name" {
description = "Project identifier used in resource naming"
type = string
}
variable "environment" {
description = "Deployment environment (dev, staging, production)"
type = string
}
variable "cloud_provider" {
description = "Target cloud provider (aws, gcp, azure, oci)"
type = string
}
variable "region" {
description = "Cloud region for deployment"
type = string
}
variable "alert_email" {
description = "Email address for alert notifications (empty to skip)"
type = string
default = ""
}
variable "log_retention_days" {
description = "Number of days to retain application logs"
type = number
default = 30
validation {
condition = contains([1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653], var.log_retention_days)
error_message = "log_retention_days must be a valid CloudWatch retention period."
}
}
variable "loadbalancer_arn" {
description = "ARN of the application load balancer to monitor"
type = string
}
variable "target_group_arns" {
description = "ARNs of target groups to monitor for unhealthy hosts"
type = list(string)
default = []
}
variable "compute_cluster_name" {
description = "Name of the ECS cluster for compute metrics"
type = string
}
variable "filesystem_id" {
description = "EFS file system ID for storage metrics"
type = string
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}
@@ -0,0 +1,279 @@
# ─────────────────────────────────────────────────────────────────────────────
# Networking module Cloud-agnostic VPC / VNet / VCN abstraction
#
# Creates the foundational network topology: virtual network, public and
# private subnets across availability zones, NAT gateway, internet gateway,
# route tables, and security groups / firewall rules.
# ─────────────────────────────────────────────────────────────────────────────
locals {
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
# Default AZs when none provided derive from region
default_azs = [
"${var.region}a",
"${var.region}b",
"${var.region}c",
]
availability_zones = length(var.availability_zones) > 0 ? var.availability_zones : local.default_azs
# Number of AZs determines subnet count
az_count = min(length(local.availability_zones), length(var.public_subnet_cidrs), length(var.private_subnet_cidrs))
common_tags = merge(
{
module = "networking"
},
var.tags,
)
}
# ─────────────────────────────────────────────────────────────────────────────
# VPC
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-vpc"
})
lifecycle {
prevent_destroy = false
}
}
# ─────────────────────────────────────────────────────────────────────────────
# Internet gateway
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-igw"
})
}
# ─────────────────────────────────────────────────────────────────────────────
# Public subnets
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_subnet" "public" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = local.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-${local.availability_zones[count.index]}"
tier = "public"
})
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-rt"
})
}
resource "aws_route_table_association" "public" {
count = local.az_count
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# ─────────────────────────────────────────────────────────────────────────────
# NAT gateway (single, in first public subnet cost-conscious default)
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_eip" "nat" {
domain = "vpc"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-nat-eip"
})
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-nat"
})
depends_on = [aws_internet_gateway.main]
}
# ─────────────────────────────────────────────────────────────────────────────
# Private subnets
# ─────────────────────────────────────────────────────────────────────────────
resource "aws_subnet" "private" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = local.availability_zones[count.index]
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-private-${local.availability_zones[count.index]}"
tier = "private"
})
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-private-rt"
})
}
resource "aws_route_table_association" "private" {
count = local.az_count
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
# ─────────────────────────────────────────────────────────────────────────────
# Security groups
# ─────────────────────────────────────────────────────────────────────────────
# Public (load-balancer-facing)
resource "aws_security_group" "public" {
name_prefix = "${local.name_prefix}-public-"
description = "Allow HTTPS/HTTP inbound and all outbound"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP (redirect)"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-sg"
})
lifecycle {
create_before_destroy = true
}
}
# Private (container-facing)
resource "aws_security_group" "private" {
name_prefix = "${local.name_prefix}-private-"
description = "Allow traffic from public SG to app and MCP ports"
vpc_id = aws_vpc.main.id
ingress {
description = "Application port from LB"
from_port = var.app_port
to_port = var.app_port
protocol = "tcp"
security_groups = [aws_security_group.public.id]
}
ingress {
description = "MCP sidecar port from LB"
from_port = var.mcp_port
to_port = var.mcp_port
protocol = "tcp"
security_groups = [aws_security_group.public.id]
}
ingress {
description = "NFS (EFS) within VPC"
from_port = 2049
to_port = 2049
protocol = "tcp"
self = true
}
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-private-sg"
})
lifecycle {
create_before_destroy = true
}
}
# EFS security group
resource "aws_security_group" "storage" {
name_prefix = "${local.name_prefix}-storage-"
description = "Allow NFS access from private security group"
vpc_id = aws_vpc.main.id
ingress {
description = "NFS from private subnets"
from_port = 2049
to_port = 2049
protocol = "tcp"
security_groups = [aws_security_group.private.id]
}
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-storage-sg"
})
lifecycle {
create_before_destroy = true
}
}
@@ -0,0 +1,53 @@
# ─────────────────────────────────────────────────────────────────────────────
# Networking module outputs
# ─────────────────────────────────────────────────────────────────────────────
output "vpc_id" {
description = "ID of the provisioned VPC"
value = aws_vpc.main.id
}
output "vpc_cidr" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs of the private subnets"
value = aws_subnet.private[*].id
}
output "public_security_group_ids" {
description = "Security group IDs for public-facing resources (LB)"
value = [aws_security_group.public.id]
}
output "private_security_group_ids" {
description = "Security group IDs for private resources (containers)"
value = [aws_security_group.private.id]
}
output "storage_security_group_ids" {
description = "Security group IDs for persistent storage"
value = [aws_security_group.storage.id]
}
output "nat_gateway_ip" {
description = "Public IP of the NAT gateway"
value = aws_eip.nat.public_ip
}
output "internet_gateway_id" {
description = "ID of the internet gateway"
value = aws_internet_gateway.main.id
}
output "availability_zones" {
description = "Availability zones used for deployment"
value = local.availability_zones
}
@@ -0,0 +1,69 @@
# ─────────────────────────────────────────────────────────────────────────────
# Networking module variables
# ─────────────────────────────────────────────────────────────────────────────
variable "project_name" {
description = "Project identifier used in resource naming"
type = string
}
variable "environment" {
description = "Deployment environment (dev, staging, production)"
type = string
}
variable "cloud_provider" {
description = "Target cloud provider (aws, gcp, azure, oci)"
type = string
}
variable "region" {
description = "Cloud region for deployment"
type = string
}
variable "vpc_cidr" {
description = "CIDR block for the virtual network"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "vpc_cidr must be a valid CIDR block."
}
}
variable "availability_zones" {
description = "List of availability zones for multi-AZ deployment"
type = list(string)
default = []
}
variable "public_subnet_cidrs" {
description = "CIDR blocks for public subnets (one per AZ)"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
variable "private_subnet_cidrs" {
description = "CIDR blocks for private subnets (one per AZ)"
type = list(string)
default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
}
variable "app_port" {
description = "Application container port"
type = number
default = 4820
}
variable "mcp_port" {
description = "MCP sidecar container port"
type = number
default = 8819
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}