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:
@@ -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 = {}
|
||||
}
|
||||
Reference in New Issue
Block a user