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,88 @@
|
||||
# Terraform Infrastructure
|
||||
|
||||
Cloud-agnostic infrastructure modules for deploying the Claude Code Agent Monitor to AWS, GCP, Azure, or OCI.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Modules["Reusable Modules"]
|
||||
NET["networking"]
|
||||
COMP["compute"]
|
||||
DB["database"]
|
||||
LB["loadbalancer"]
|
||||
MON["monitoring"]
|
||||
SEC["secrets"]
|
||||
end
|
||||
|
||||
subgraph Providers["Provider Implementations"]
|
||||
AWS["aws/"]
|
||||
GCP["gcp/"]
|
||||
AZ["azure/"]
|
||||
OCI["oci/"]
|
||||
end
|
||||
|
||||
subgraph Envs["Environments"]
|
||||
DEV["dev/terraform.tfvars"]
|
||||
STG["staging/terraform.tfvars"]
|
||||
PRD["production/terraform.tfvars"]
|
||||
end
|
||||
|
||||
AWS --> NET & COMP & DB & LB & MON & SEC
|
||||
GCP --> NET & COMP & DB & LB & MON & SEC
|
||||
AZ --> NET & COMP & DB & LB & MON & SEC
|
||||
OCI --> NET & COMP & DB & LB & MON & SEC
|
||||
Envs -.->|var-file| Providers
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# 1. Choose your provider
|
||||
cd providers/aws # or gcp, azure, oci
|
||||
|
||||
# 2. Initialize
|
||||
terraform init
|
||||
|
||||
# 3. Plan with environment
|
||||
terraform plan -var-file=../../environments/production/terraform.tfvars
|
||||
|
||||
# 4. Apply
|
||||
terraform apply -var-file=../../environments/production/terraform.tfvars
|
||||
|
||||
# 5. Get outputs
|
||||
terraform output
|
||||
```
|
||||
|
||||
## Module Reference
|
||||
|
||||
| Module | Purpose | Key Resources |
|
||||
|---|---|---|
|
||||
| `networking` | VPC/VNet, subnets, NAT, security groups | VPC, public/private subnets, NAT gateway, firewall rules |
|
||||
| `compute` | Container orchestration with blue-green slots | ECS tasks / Cloud Run / ACI / OKE deployments |
|
||||
| `database` | Persistent storage for SQLite | EFS / Filestore / Azure Files / FSS with encryption |
|
||||
| `loadbalancer` | Application LB with WebSocket + traffic splitting | ALB / GCLB / App Gateway / LBaaS, health checks |
|
||||
| `monitoring` | Metrics, logs, alerts, dashboards | CloudWatch / Cloud Monitoring / Azure Monitor / OCI Monitoring |
|
||||
| `secrets` | Secret management | Secrets Manager / Secret Manager / Key Vault / Vault |
|
||||
|
||||
## Remote State
|
||||
|
||||
Each provider is configured to use cloud-native remote state:
|
||||
|
||||
| Provider | Backend | Bucket |
|
||||
|---|---|---|
|
||||
| AWS | S3 + DynamoDB locking | `agent-monitor-tfstate-{account_id}` |
|
||||
| GCP | GCS | `agent-monitor-tfstate-{project_id}` |
|
||||
| Azure | Azure Blob Storage | `agentmonitortfstate` |
|
||||
| OCI | OCI Object Storage | `agent-monitor-tfstate` |
|
||||
|
||||
## Environment Sizing
|
||||
|
||||
| Resource | Dev | Staging | Production |
|
||||
|---|---|---|---|
|
||||
| Replicas | 1 | 2 | 3 (auto-scale to 10) |
|
||||
| CPU | 256 | 512 | 1024 |
|
||||
| Memory | 512 MB | 1 GB | 2 GB |
|
||||
| Storage | 5 GB | 10 GB | 50 GB (encrypted) |
|
||||
| Multi-AZ | No | Yes | Yes |
|
||||
| Monitoring | Basic | Standard | Full + alerts |
|
||||
@@ -0,0 +1,47 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Remote state backend – uncomment the block matching your cloud provider.
|
||||
# Only ONE backend may be active at a time.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── AWS S3 ──────────────────────────────────────────────────────────────────
|
||||
# terraform {
|
||||
# backend "s3" {
|
||||
# bucket = "ccam-terraform-state"
|
||||
# key = "claude-agent-monitor/terraform.tfstate"
|
||||
# region = "us-east-1"
|
||||
# encrypt = true
|
||||
# dynamodb_table = "ccam-terraform-locks"
|
||||
# }
|
||||
# }
|
||||
|
||||
# ── GCP Cloud Storage ──────────────────────────────────────────────────────
|
||||
# terraform {
|
||||
# backend "gcs" {
|
||||
# bucket = "ccam-terraform-state"
|
||||
# prefix = "claude-agent-monitor"
|
||||
# }
|
||||
# }
|
||||
|
||||
# ── Azure Blob Storage ─────────────────────────────────────────────────────
|
||||
# terraform {
|
||||
# backend "azurerm" {
|
||||
# resource_group_name = "ccam-terraform-state-rg"
|
||||
# storage_account_name = "ccamtfstate"
|
||||
# container_name = "tfstate"
|
||||
# key = "claude-agent-monitor.tfstate"
|
||||
# }
|
||||
# }
|
||||
|
||||
# ── OCI Object Storage ─────────────────────────────────────────────────────
|
||||
# terraform {
|
||||
# backend "s3" {
|
||||
# bucket = "ccam-terraform-state"
|
||||
# key = "claude-agent-monitor/terraform.tfstate"
|
||||
# region = "us-ashburn-1"
|
||||
# endpoint = "https://<namespace>.compat.objectstorage.<region>.oraclecloud.com"
|
||||
# skip_region_validation = true
|
||||
# skip_credentials_validation = true
|
||||
# skip_metadata_api_check = true
|
||||
# force_path_style = true
|
||||
# }
|
||||
# }
|
||||
@@ -0,0 +1,67 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Development environment – terraform.tfvars
|
||||
#
|
||||
# Minimal resources for development/testing. Single replica, small compute,
|
||||
# monitoring disabled to reduce cost.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Provider ────────────────────────────────────────────────────────────────
|
||||
cloud_provider = "aws"
|
||||
region = "us-east-1"
|
||||
|
||||
# ── Project ─────────────────────────────────────────────────────────────────
|
||||
project_name = "claude-agent-monitor"
|
||||
environment = "dev"
|
||||
|
||||
tags = {
|
||||
team = "platform"
|
||||
cost_center = "engineering"
|
||||
}
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────────────────────
|
||||
vpc_cidr = "10.0.0.0/16"
|
||||
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
|
||||
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
|
||||
|
||||
# ── Compute (small) ────────────────────────────────────────────────────────
|
||||
app_container_image = "ghcr.io/anthropics/claude-agent-monitor:latest"
|
||||
mcp_container_image = "" # MCP sidecar disabled in dev
|
||||
cpu = 256 # 0.25 vCPU
|
||||
memory = 512 # 512 MiB
|
||||
|
||||
min_replicas = 1
|
||||
max_replicas = 1
|
||||
desired_replicas = 1
|
||||
|
||||
environment_variables = {
|
||||
NODE_ENV = "development"
|
||||
DASHBOARD_PORT = "4820"
|
||||
LOG_LEVEL = "debug"
|
||||
}
|
||||
|
||||
# ── Deployment ──────────────────────────────────────────────────────────────
|
||||
deployment_strategy = "rolling"
|
||||
active_deployment_slot = "blue"
|
||||
blue_weight = 100
|
||||
green_weight = 0
|
||||
|
||||
# ── TLS (disabled in dev) ──────────────────────────────────────────────────
|
||||
domain_name = ""
|
||||
tls_certificate_arn = ""
|
||||
|
||||
# ── Storage ─────────────────────────────────────────────────────────────────
|
||||
storage_size_gb = 10
|
||||
enable_storage_backup = false
|
||||
|
||||
# ── Health check ────────────────────────────────────────────────────────────
|
||||
health_check_path = "/api/health"
|
||||
health_check_interval = 60 # Less frequent in dev
|
||||
|
||||
# ── Auto-scaling (disabled – single replica) ────────────────────────────────
|
||||
autoscaling_cpu_target = 80
|
||||
autoscaling_memory_target = 90
|
||||
|
||||
# ── Monitoring (minimal) ───────────────────────────────────────────────────
|
||||
enable_monitoring = false
|
||||
alert_email = ""
|
||||
log_retention_days = 7
|
||||
@@ -0,0 +1,80 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Production environment – terraform.tfvars
|
||||
#
|
||||
# Full production configuration. 3+ replicas with auto-scaling, large
|
||||
# compute, comprehensive monitoring, TLS, blue-green deployment ready.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Provider ────────────────────────────────────────────────────────────────
|
||||
cloud_provider = "aws"
|
||||
region = "us-east-1"
|
||||
|
||||
# ── Project ─────────────────────────────────────────────────────────────────
|
||||
project_name = "claude-agent-monitor"
|
||||
environment = "production"
|
||||
|
||||
tags = {
|
||||
team = "platform"
|
||||
cost_center = "engineering"
|
||||
criticality = "high"
|
||||
compliance = "soc2"
|
||||
}
|
||||
|
||||
# ── Networking (3 AZs for high availability) ───────────────────────────────
|
||||
vpc_cidr = "10.2.0.0/16"
|
||||
public_subnet_cidrs = ["10.2.1.0/24", "10.2.2.0/24", "10.2.3.0/24"]
|
||||
private_subnet_cidrs = ["10.2.11.0/24", "10.2.12.0/24", "10.2.13.0/24"]
|
||||
|
||||
# ── Compute (large) ────────────────────────────────────────────────────────
|
||||
app_container_image = "ghcr.io/anthropics/claude-agent-monitor:latest"
|
||||
mcp_container_image = "ghcr.io/anthropics/claude-agent-monitor-mcp:latest"
|
||||
cpu = 1024 # 1 vCPU
|
||||
memory = 2048 # 2 GiB
|
||||
|
||||
min_replicas = 3
|
||||
max_replicas = 10
|
||||
desired_replicas = 3
|
||||
|
||||
environment_variables = {
|
||||
NODE_ENV = "production"
|
||||
DASHBOARD_PORT = "4820"
|
||||
LOG_LEVEL = "warn"
|
||||
}
|
||||
|
||||
# ── Deployment (blue-green with canary support) ────────────────────────────
|
||||
deployment_strategy = "blue-green"
|
||||
active_deployment_slot = "blue"
|
||||
blue_weight = 100
|
||||
green_weight = 0
|
||||
|
||||
# During canary deployment, adjust weights:
|
||||
# blue_weight = 90
|
||||
# green_weight = 10
|
||||
# Then gradually shift to:
|
||||
# blue_weight = 0
|
||||
# green_weight = 100
|
||||
# Finally, flip active_deployment_slot = "green"
|
||||
|
||||
# ── TLS ─────────────────────────────────────────────────────────────────────
|
||||
domain_name = "" # Set to production FQDN (e.g. "monitor.example.com")
|
||||
tls_certificate_arn = "" # Set to existing ACM cert ARN or leave empty for auto
|
||||
|
||||
# ── Storage ─────────────────────────────────────────────────────────────────
|
||||
storage_size_gb = 50
|
||||
enable_storage_backup = true
|
||||
|
||||
# ── Health check (strict thresholds) ───────────────────────────────────────
|
||||
health_check_path = "/api/health"
|
||||
health_check_interval = 15
|
||||
health_check_timeout = 5
|
||||
health_check_healthy_threshold = 2
|
||||
health_check_unhealthy_threshold = 2
|
||||
|
||||
# ── Auto-scaling (aggressive) ──────────────────────────────────────────────
|
||||
autoscaling_cpu_target = 60
|
||||
autoscaling_memory_target = 70
|
||||
|
||||
# ── Monitoring (comprehensive) ─────────────────────────────────────────────
|
||||
enable_monitoring = true
|
||||
alert_email = "" # REQUIRED: Set to ops team email for production alerts
|
||||
log_retention_days = 90
|
||||
@@ -0,0 +1,70 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Staging environment – terraform.tfvars
|
||||
#
|
||||
# Production-like configuration with moderate resources. Two replicas,
|
||||
# medium compute, monitoring enabled with relaxed thresholds.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Provider ────────────────────────────────────────────────────────────────
|
||||
cloud_provider = "aws"
|
||||
region = "us-east-1"
|
||||
|
||||
# ── Project ─────────────────────────────────────────────────────────────────
|
||||
project_name = "claude-agent-monitor"
|
||||
environment = "staging"
|
||||
|
||||
tags = {
|
||||
team = "platform"
|
||||
cost_center = "engineering"
|
||||
}
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────────────────────
|
||||
vpc_cidr = "10.1.0.0/16"
|
||||
public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
|
||||
private_subnet_cidrs = ["10.1.11.0/24", "10.1.12.0/24", "10.1.13.0/24"]
|
||||
|
||||
# ── Compute (medium) ───────────────────────────────────────────────────────
|
||||
app_container_image = "ghcr.io/anthropics/claude-agent-monitor:staging"
|
||||
mcp_container_image = "ghcr.io/anthropics/claude-agent-monitor-mcp:staging"
|
||||
cpu = 512 # 0.5 vCPU
|
||||
memory = 1024 # 1 GiB
|
||||
|
||||
min_replicas = 1
|
||||
max_replicas = 3
|
||||
desired_replicas = 2
|
||||
|
||||
environment_variables = {
|
||||
NODE_ENV = "production"
|
||||
DASHBOARD_PORT = "4820"
|
||||
LOG_LEVEL = "info"
|
||||
}
|
||||
|
||||
# ── Deployment (blue-green ready) ──────────────────────────────────────────
|
||||
deployment_strategy = "blue-green"
|
||||
active_deployment_slot = "blue"
|
||||
blue_weight = 100
|
||||
green_weight = 0
|
||||
|
||||
# ── TLS ─────────────────────────────────────────────────────────────────────
|
||||
domain_name = "" # Set to staging FQDN when available
|
||||
tls_certificate_arn = "" # Auto-created if domain_name is set
|
||||
|
||||
# ── Storage ─────────────────────────────────────────────────────────────────
|
||||
storage_size_gb = 20
|
||||
enable_storage_backup = true
|
||||
|
||||
# ── Health check ────────────────────────────────────────────────────────────
|
||||
health_check_path = "/api/health"
|
||||
health_check_interval = 30
|
||||
health_check_timeout = 5
|
||||
health_check_healthy_threshold = 2
|
||||
health_check_unhealthy_threshold = 3
|
||||
|
||||
# ── Auto-scaling ────────────────────────────────────────────────────────────
|
||||
autoscaling_cpu_target = 70
|
||||
autoscaling_memory_target = 80
|
||||
|
||||
# ── Monitoring ──────────────────────────────────────────────────────────────
|
||||
enable_monitoring = true
|
||||
alert_email = "" # Set to team email for staging alerts
|
||||
log_retention_days = 14
|
||||
@@ -0,0 +1,185 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Claude Code Agent Monitor – Root orchestration module
|
||||
#
|
||||
# Selects the cloud provider implementation via var.cloud_provider and wires
|
||||
# the generic modules together. Each provider directory contains a full,
|
||||
# opinionated implementation that composes the child modules.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
locals {
|
||||
common_tags = merge(
|
||||
{
|
||||
project = var.project_name
|
||||
environment = var.environment
|
||||
managed_by = "terraform"
|
||||
repository = "Claude-Code-Agent-Monitor"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
|
||||
# Normalise resource name prefix (lowercase, hyphens)
|
||||
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
|
||||
}
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────────────────────
|
||||
|
||||
module "networking" {
|
||||
source = "./modules/networking"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = var.cloud_provider
|
||||
region = var.region
|
||||
vpc_cidr = var.vpc_cidr
|
||||
availability_zones = var.availability_zones
|
||||
public_subnet_cidrs = var.public_subnet_cidrs
|
||||
private_subnet_cidrs = var.private_subnet_cidrs
|
||||
app_port = var.app_port
|
||||
mcp_port = var.mcp_port
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ── Persistent storage (SQLite DB) ─────────────────────────────────────────
|
||||
|
||||
module "database" {
|
||||
source = "./modules/database"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = var.cloud_provider
|
||||
region = var.region
|
||||
storage_size_gb = var.storage_size_gb
|
||||
enable_backup = var.enable_storage_backup
|
||||
private_subnet_ids = module.networking.private_subnet_ids
|
||||
vpc_id = module.networking.vpc_id
|
||||
allowed_security_group_ids = module.networking.storage_security_group_ids
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ── Compute (Blue slot) ────────────────────────────────────────────────────
|
||||
|
||||
module "compute_blue" {
|
||||
source = "./modules/compute"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = var.cloud_provider
|
||||
region = var.region
|
||||
deployment_slot = "blue"
|
||||
container_image = var.app_container_image
|
||||
mcp_container_image = var.mcp_container_image
|
||||
app_port = var.app_port
|
||||
mcp_port = var.mcp_port
|
||||
cpu = var.cpu
|
||||
memory = var.memory
|
||||
desired_count = var.active_deployment_slot == "blue" ? var.desired_replicas : 0
|
||||
min_count = var.active_deployment_slot == "blue" ? var.min_replicas : 0
|
||||
max_count = var.active_deployment_slot == "blue" ? var.max_replicas : 0
|
||||
environment_variables = var.environment_variables
|
||||
health_check_path = var.health_check_path
|
||||
vpc_id = module.networking.vpc_id
|
||||
private_subnet_ids = module.networking.private_subnet_ids
|
||||
security_group_ids = module.networking.private_security_group_ids
|
||||
storage_filesystem_id = module.database.filesystem_id
|
||||
storage_mount_targets = module.database.mount_target_ids
|
||||
autoscaling_cpu_target = var.autoscaling_cpu_target
|
||||
autoscaling_memory_target = var.autoscaling_memory_target
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ── Compute (Green slot) ───────────────────────────────────────────────────
|
||||
|
||||
module "compute_green" {
|
||||
source = "./modules/compute"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = var.cloud_provider
|
||||
region = var.region
|
||||
deployment_slot = "green"
|
||||
container_image = var.app_container_image
|
||||
mcp_container_image = var.mcp_container_image
|
||||
app_port = var.app_port
|
||||
mcp_port = var.mcp_port
|
||||
cpu = var.cpu
|
||||
memory = var.memory
|
||||
desired_count = var.active_deployment_slot == "green" ? var.desired_replicas : 0
|
||||
min_count = var.active_deployment_slot == "green" ? var.min_replicas : 0
|
||||
max_count = var.active_deployment_slot == "green" ? var.max_replicas : 0
|
||||
environment_variables = var.environment_variables
|
||||
health_check_path = var.health_check_path
|
||||
vpc_id = module.networking.vpc_id
|
||||
private_subnet_ids = module.networking.private_subnet_ids
|
||||
security_group_ids = module.networking.private_security_group_ids
|
||||
storage_filesystem_id = module.database.filesystem_id
|
||||
storage_mount_targets = module.database.mount_target_ids
|
||||
autoscaling_cpu_target = var.autoscaling_cpu_target
|
||||
autoscaling_memory_target = var.autoscaling_memory_target
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ── Load balancer ───────────────────────────────────────────────────────────
|
||||
|
||||
module "loadbalancer" {
|
||||
source = "./modules/loadbalancer"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = var.cloud_provider
|
||||
region = var.region
|
||||
vpc_id = module.networking.vpc_id
|
||||
public_subnet_ids = module.networking.public_subnet_ids
|
||||
security_group_ids = module.networking.public_security_group_ids
|
||||
app_port = var.app_port
|
||||
mcp_port = var.mcp_port
|
||||
tls_certificate_arn = var.tls_certificate_arn
|
||||
domain_name = var.domain_name
|
||||
|
||||
blue_target_group_arn = module.compute_blue.target_group_arn
|
||||
green_target_group_arn = module.compute_green.target_group_arn
|
||||
blue_weight = var.blue_weight
|
||||
green_weight = var.green_weight
|
||||
|
||||
health_check_path = var.health_check_path
|
||||
health_check_interval = var.health_check_interval
|
||||
health_check_timeout = var.health_check_timeout
|
||||
health_check_healthy_threshold = var.health_check_healthy_threshold
|
||||
health_check_unhealthy_threshold = var.health_check_unhealthy_threshold
|
||||
|
||||
enable_deletion_protection = var.environment == "production"
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ── Weight sum validation ───────────────────────────────────────────────────
|
||||
|
||||
check "blue_green_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."
|
||||
}
|
||||
}
|
||||
|
||||
# ── Monitoring ──────────────────────────────────────────────────────────────
|
||||
|
||||
module "monitoring" {
|
||||
source = "./modules/monitoring"
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = var.cloud_provider
|
||||
region = var.region
|
||||
alert_email = var.alert_email
|
||||
log_retention_days = var.log_retention_days
|
||||
|
||||
loadbalancer_arn = module.loadbalancer.loadbalancer_arn
|
||||
target_group_arns = [
|
||||
module.compute_blue.target_group_arn,
|
||||
module.compute_green.target_group_arn,
|
||||
]
|
||||
compute_cluster_name = module.compute_blue.cluster_name
|
||||
filesystem_id = module.database.filesystem_id
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
@@ -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 = {}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Root module outputs – Claude Code Agent Monitor
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
output "application_url" {
|
||||
description = "Public URL of the Claude Code Agent Monitor dashboard"
|
||||
value = module.loadbalancer.application_url
|
||||
}
|
||||
|
||||
output "loadbalancer_dns" {
|
||||
description = "DNS name of the application load balancer"
|
||||
value = module.loadbalancer.dns_name
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "ID of the provisioned VPC / VNet / VCN"
|
||||
value = module.networking.vpc_id
|
||||
}
|
||||
|
||||
output "private_subnet_ids" {
|
||||
description = "IDs of the private subnets hosting compute workloads"
|
||||
value = module.networking.private_subnet_ids
|
||||
}
|
||||
|
||||
output "public_subnet_ids" {
|
||||
description = "IDs of the public subnets hosting the load balancer"
|
||||
value = module.networking.public_subnet_ids
|
||||
}
|
||||
|
||||
output "filesystem_id" {
|
||||
description = "ID of the persistent file system for SQLite storage"
|
||||
value = module.database.filesystem_id
|
||||
}
|
||||
|
||||
output "blue_service_name" {
|
||||
description = "Name of the blue deployment compute service"
|
||||
value = module.compute_blue.service_name
|
||||
}
|
||||
|
||||
output "green_service_name" {
|
||||
description = "Name of the green deployment compute service"
|
||||
value = module.compute_green.service_name
|
||||
}
|
||||
|
||||
output "active_slot" {
|
||||
description = "Currently active deployment slot"
|
||||
value = var.active_deployment_slot
|
||||
}
|
||||
|
||||
output "monitoring_dashboard_url" {
|
||||
description = "URL of the monitoring dashboard (if enabled)"
|
||||
value = var.enable_monitoring ? module.monitoring[0].dashboard_url : "monitoring disabled"
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AWS Provider – Full implementation for Claude Code Agent Monitor
|
||||
#
|
||||
# Composes the generic modules into a production-ready AWS stack:
|
||||
# VPC → ECS Fargate → EFS → ALB → CloudWatch → ACM
|
||||
#
|
||||
# Features:
|
||||
# - Multi-AZ deployment
|
||||
# - Blue/green deployment slots
|
||||
# - EFS for persistent SQLite storage
|
||||
# - ALB with WebSocket support and sticky sessions
|
||||
# - Auto-scaling with CPU/memory targets
|
||||
# - CloudWatch monitoring, alarms, and dashboards
|
||||
# - IAM least-privilege roles
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
|
||||
default_tags {
|
||||
tags = local.common_tags
|
||||
}
|
||||
}
|
||||
|
||||
# ── Data sources ────────────────────────────────────────────────────────────
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
data "aws_availability_zones" "available" {
|
||||
state = "available"
|
||||
}
|
||||
|
||||
# ── Locals ──────────────────────────────────────────────────────────────────
|
||||
|
||||
locals {
|
||||
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
|
||||
|
||||
common_tags = merge(
|
||||
{
|
||||
project = var.project_name
|
||||
environment = var.environment
|
||||
managed_by = "terraform"
|
||||
cloud_provider = "aws"
|
||||
repository = "Claude-Code-Agent-Monitor"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
|
||||
# Use first 3 available AZs when none specified
|
||||
availability_zones = length(var.availability_zones) > 0 ? var.availability_zones : slice(data.aws_availability_zones.available.names, 0, min(3, length(data.aws_availability_zones.available.names)))
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Networking
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
module "networking" {
|
||||
source = "../../modules/networking"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = "aws"
|
||||
region = var.region
|
||||
vpc_cidr = var.vpc_cidr
|
||||
availability_zones = local.availability_zones
|
||||
public_subnet_cidrs = var.public_subnet_cidrs
|
||||
private_subnet_cidrs = var.private_subnet_cidrs
|
||||
app_port = var.app_port
|
||||
mcp_port = var.mcp_port
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Database (EFS for SQLite persistence)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
module "database" {
|
||||
source = "../../modules/database"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = "aws"
|
||||
region = var.region
|
||||
storage_size_gb = var.storage_size_gb
|
||||
enable_backup = var.enable_storage_backup
|
||||
private_subnet_ids = module.networking.private_subnet_ids
|
||||
vpc_id = module.networking.vpc_id
|
||||
allowed_security_group_ids = module.networking.storage_security_group_ids
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Compute – Blue slot
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
module "compute_blue" {
|
||||
source = "../../modules/compute"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = "aws"
|
||||
region = var.region
|
||||
deployment_slot = "blue"
|
||||
container_image = var.app_container_image
|
||||
mcp_container_image = var.mcp_container_image
|
||||
app_port = var.app_port
|
||||
mcp_port = var.mcp_port
|
||||
cpu = var.cpu
|
||||
memory = var.memory
|
||||
desired_count = var.active_deployment_slot == "blue" ? var.desired_replicas : 0
|
||||
min_count = var.active_deployment_slot == "blue" ? var.min_replicas : 0
|
||||
max_count = var.active_deployment_slot == "blue" ? var.max_replicas : 0
|
||||
environment_variables = var.environment_variables
|
||||
health_check_path = var.health_check_path
|
||||
vpc_id = module.networking.vpc_id
|
||||
private_subnet_ids = module.networking.private_subnet_ids
|
||||
security_group_ids = module.networking.private_security_group_ids
|
||||
storage_filesystem_id = module.database.filesystem_id
|
||||
storage_mount_targets = module.database.mount_target_ids
|
||||
autoscaling_cpu_target = var.autoscaling_cpu_target
|
||||
autoscaling_memory_target = var.autoscaling_memory_target
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Compute – Green slot
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
module "compute_green" {
|
||||
source = "../../modules/compute"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = "aws"
|
||||
region = var.region
|
||||
deployment_slot = "green"
|
||||
container_image = var.app_container_image
|
||||
mcp_container_image = var.mcp_container_image
|
||||
app_port = var.app_port
|
||||
mcp_port = var.mcp_port
|
||||
cpu = var.cpu
|
||||
memory = var.memory
|
||||
desired_count = var.active_deployment_slot == "green" ? var.desired_replicas : 0
|
||||
min_count = var.active_deployment_slot == "green" ? var.min_replicas : 0
|
||||
max_count = var.active_deployment_slot == "green" ? var.max_replicas : 0
|
||||
environment_variables = var.environment_variables
|
||||
health_check_path = var.health_check_path
|
||||
vpc_id = module.networking.vpc_id
|
||||
private_subnet_ids = module.networking.private_subnet_ids
|
||||
security_group_ids = module.networking.private_security_group_ids
|
||||
storage_filesystem_id = module.database.filesystem_id
|
||||
storage_mount_targets = module.database.mount_target_ids
|
||||
autoscaling_cpu_target = var.autoscaling_cpu_target
|
||||
autoscaling_memory_target = var.autoscaling_memory_target
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ACM Certificate (optional – when domain_name is specified)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "aws_acm_certificate" "main" {
|
||||
count = var.domain_name != "" && var.tls_certificate_arn == "" ? 1 : 0
|
||||
|
||||
domain_name = var.domain_name
|
||||
validation_method = "DNS"
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
Name = "${local.name_prefix}-cert"
|
||||
})
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
tls_cert_arn = var.tls_certificate_arn != "" ? var.tls_certificate_arn : (
|
||||
length(aws_acm_certificate.main) > 0 ? aws_acm_certificate.main[0].arn : ""
|
||||
)
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Load Balancer
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
module "loadbalancer" {
|
||||
source = "../../modules/loadbalancer"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = "aws"
|
||||
region = var.region
|
||||
vpc_id = module.networking.vpc_id
|
||||
public_subnet_ids = module.networking.public_subnet_ids
|
||||
security_group_ids = module.networking.public_security_group_ids
|
||||
app_port = var.app_port
|
||||
mcp_port = var.mcp_port
|
||||
tls_certificate_arn = local.tls_cert_arn
|
||||
domain_name = var.domain_name
|
||||
|
||||
blue_target_group_arn = module.compute_blue.target_group_arn
|
||||
green_target_group_arn = module.compute_green.target_group_arn
|
||||
blue_weight = var.blue_weight
|
||||
green_weight = var.green_weight
|
||||
|
||||
health_check_path = var.health_check_path
|
||||
health_check_interval = var.health_check_interval
|
||||
health_check_timeout = var.health_check_timeout
|
||||
health_check_healthy_threshold = var.health_check_healthy_threshold
|
||||
health_check_unhealthy_threshold = var.health_check_unhealthy_threshold
|
||||
|
||||
enable_deletion_protection = var.environment == "production"
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Monitoring
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
module "monitoring" {
|
||||
source = "../../modules/monitoring"
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
cloud_provider = "aws"
|
||||
region = var.region
|
||||
alert_email = var.alert_email
|
||||
log_retention_days = var.log_retention_days
|
||||
|
||||
loadbalancer_arn = module.loadbalancer.loadbalancer_arn
|
||||
target_group_arns = [module.compute_blue.target_group_arn, module.compute_green.target_group_arn]
|
||||
compute_cluster_name = module.compute_blue.cluster_name
|
||||
filesystem_id = module.database.filesystem_id
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Route53 DNS record (optional)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
data "aws_route53_zone" "main" {
|
||||
count = var.domain_name != "" && var.route53_zone_id != "" ? 1 : 0
|
||||
|
||||
zone_id = var.route53_zone_id
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "app" {
|
||||
count = var.domain_name != "" && var.route53_zone_id != "" ? 1 : 0
|
||||
|
||||
zone_id = data.aws_route53_zone.main[0].zone_id
|
||||
name = var.domain_name
|
||||
type = "A"
|
||||
|
||||
alias {
|
||||
name = module.loadbalancer.dns_name
|
||||
zone_id = module.loadbalancer.zone_id
|
||||
evaluate_target_health = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AWS provider outputs
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
output "application_url" {
|
||||
description = "Public URL of the Claude Code Agent Monitor dashboard"
|
||||
value = module.loadbalancer.application_url
|
||||
}
|
||||
|
||||
output "alb_dns_name" {
|
||||
description = "DNS name of the Application Load Balancer"
|
||||
value = module.loadbalancer.dns_name
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "ID of the VPC"
|
||||
value = module.networking.vpc_id
|
||||
}
|
||||
|
||||
output "ecs_cluster_name" {
|
||||
description = "Name of the ECS cluster"
|
||||
value = module.compute_blue.cluster_name
|
||||
}
|
||||
|
||||
output "blue_service_name" {
|
||||
description = "Name of the blue ECS service"
|
||||
value = module.compute_blue.service_name
|
||||
}
|
||||
|
||||
output "green_service_name" {
|
||||
description = "Name of the green ECS service"
|
||||
value = module.compute_green.service_name
|
||||
}
|
||||
|
||||
output "efs_filesystem_id" {
|
||||
description = "ID of the EFS file system"
|
||||
value = module.database.filesystem_id
|
||||
}
|
||||
|
||||
output "acm_certificate_arn" {
|
||||
description = "ARN of the ACM certificate (if auto-created)"
|
||||
value = length(aws_acm_certificate.main) > 0 ? aws_acm_certificate.main[0].arn : var.tls_certificate_arn
|
||||
}
|
||||
|
||||
output "monitoring_dashboard_url" {
|
||||
description = "CloudWatch dashboard URL"
|
||||
value = var.enable_monitoring ? module.monitoring[0].dashboard_url : "monitoring disabled"
|
||||
}
|
||||
|
||||
output "account_id" {
|
||||
description = "AWS account ID"
|
||||
value = data.aws_caller_identity.current.account_id
|
||||
}
|
||||
|
||||
output "region" {
|
||||
description = "AWS region"
|
||||
value = data.aws_region.current.name
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AWS provider – Terraform and provider constraints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AWS provider variables
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Core ────────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project identifier used in resource naming and tagging"
|
||||
type = string
|
||||
default = "claude-agent-monitor"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment: dev, staging, or production"
|
||||
type = string
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "production"], var.environment)
|
||||
error_message = "environment must be one of: dev, staging, production."
|
||||
}
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "AWS region for resource deployment"
|
||||
type = string
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Additional tags to apply to all resources"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "vpc_cidr" {
|
||||
description = "CIDR block for the VPC"
|
||||
type = string
|
||||
default = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
variable "availability_zones" {
|
||||
description = "List of AZs (auto-detected if empty)"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "public_subnet_cidrs" {
|
||||
description = "CIDR blocks for public subnets"
|
||||
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"
|
||||
type = list(string)
|
||||
default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
|
||||
}
|
||||
|
||||
# ── Compute ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_container_image" {
|
||||
description = "Docker image URI for the main application"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "mcp_container_image" {
|
||||
description = "Docker image URI for the MCP sidecar (empty to disable)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "app_port" {
|
||||
description = "Application container port"
|
||||
type = number
|
||||
default = 4820
|
||||
}
|
||||
|
||||
variable "mcp_port" {
|
||||
description = "MCP sidecar container port"
|
||||
type = number
|
||||
default = 8819
|
||||
}
|
||||
|
||||
variable "cpu" {
|
||||
description = "CPU units for Fargate tasks (256, 512, 1024, 2048, 4096)"
|
||||
type = number
|
||||
default = 512
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory in MiB for Fargate tasks"
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "min_replicas" {
|
||||
description = "Minimum number of ECS tasks"
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "max_replicas" {
|
||||
description = "Maximum number of ECS tasks for auto-scaling"
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
variable "desired_replicas" {
|
||||
description = "Desired number of ECS tasks at steady state"
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "environment_variables" {
|
||||
description = "Environment variables for the application container"
|
||||
type = map(string)
|
||||
default = {
|
||||
NODE_ENV = "production"
|
||||
DASHBOARD_PORT = "4820"
|
||||
}
|
||||
}
|
||||
|
||||
# ── Deployment ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "active_deployment_slot" {
|
||||
description = "Active deployment slot: blue or green"
|
||||
type = string
|
||||
default = "blue"
|
||||
validation {
|
||||
condition = contains(["blue", "green"], var.active_deployment_slot)
|
||||
error_message = "active_deployment_slot must be blue or green."
|
||||
}
|
||||
}
|
||||
|
||||
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."
|
||||
}
|
||||
}
|
||||
|
||||
# ── TLS / Domain ────────────────────────────────────────────────────────────
|
||||
|
||||
variable "domain_name" {
|
||||
description = "FQDN for the application (empty to skip DNS/TLS)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "tls_certificate_arn" {
|
||||
description = "ARN of an existing ACM certificate (auto-created if domain_name set)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "route53_zone_id" {
|
||||
description = "Route53 hosted zone ID for DNS records (empty to skip)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ── Storage ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "storage_size_gb" {
|
||||
description = "EFS storage does not require pre-provisioning; kept for interface compatibility"
|
||||
type = number
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "enable_storage_backup" {
|
||||
description = "Enable AWS Backup for EFS"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
# ── Health check ────────────────────────────────────────────────────────────
|
||||
|
||||
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 request times out"
|
||||
type = number
|
||||
default = 5
|
||||
}
|
||||
|
||||
variable "health_check_healthy_threshold" {
|
||||
description = "Consecutive successes to mark target healthy"
|
||||
type = number
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "health_check_unhealthy_threshold" {
|
||||
description = "Consecutive failures to mark target unhealthy"
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
# ── Auto-scaling ────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# ── Monitoring ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "enable_monitoring" {
|
||||
description = "Enable CloudWatch monitoring, alarms, and dashboards"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "alert_email" {
|
||||
description = "Email address for SNS alert notifications"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "log_retention_days" {
|
||||
description = "CloudWatch log retention in days"
|
||||
type = number
|
||||
default = 30
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Azure Provider – Full implementation for Claude Code Agent Monitor
|
||||
#
|
||||
# Architecture:
|
||||
# VNet → ACI (Container Instances) or AKS → Azure Files → Application
|
||||
# Gateway → Azure Monitor → Key Vault
|
||||
#
|
||||
# Azure Container Instances is chosen for simplicity; for production at
|
||||
# scale, AKS is recommended. Application Gateway provides L7 LB with
|
||||
# WebSocket support and SSL termination.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
provider "azurerm" {
|
||||
features {
|
||||
resource_group {
|
||||
prevent_deletion_if_contains_resources = true
|
||||
}
|
||||
key_vault {
|
||||
purge_soft_delete_on_destroy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Data sources ────────────────────────────────────────────────────────────
|
||||
|
||||
data "azurerm_client_config" "current" {}
|
||||
|
||||
# ── Locals ──────────────────────────────────────────────────────────────────
|
||||
|
||||
locals {
|
||||
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
|
||||
|
||||
# Azure resource names (alphanumeric for storage accounts)
|
||||
storage_account_name = lower(replace(substr("ccam${var.environment}${substr(md5(var.project_name), 0, 8)}", 0, 24), "-", ""))
|
||||
|
||||
common_tags = merge(
|
||||
{
|
||||
project = var.project_name
|
||||
environment = var.environment
|
||||
managed_by = "terraform"
|
||||
cloud_provider = "azure"
|
||||
repository = "Claude-Code-Agent-Monitor"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Resource Group
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "azurerm_resource_group" "main" {
|
||||
name = "${local.name_prefix}-rg"
|
||||
location = var.region
|
||||
|
||||
tags = local.common_tags
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Virtual Network
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "azurerm_virtual_network" "main" {
|
||||
name = "${local.name_prefix}-vnet"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
location = azurerm_resource_group.main.location
|
||||
address_space = [var.vpc_cidr]
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "azurerm_subnet" "public" {
|
||||
name = "${local.name_prefix}-public"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
virtual_network_name = azurerm_virtual_network.main.name
|
||||
address_prefixes = [var.public_subnet_cidrs[0]]
|
||||
}
|
||||
|
||||
resource "azurerm_subnet" "private" {
|
||||
name = "${local.name_prefix}-private"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
virtual_network_name = azurerm_virtual_network.main.name
|
||||
address_prefixes = [var.private_subnet_cidrs[0]]
|
||||
|
||||
delegation {
|
||||
name = "aci-delegation"
|
||||
service_delegation {
|
||||
name = "Microsoft.ContainerInstance/containerGroups"
|
||||
actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_subnet" "appgw" {
|
||||
name = "${local.name_prefix}-appgw"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
virtual_network_name = azurerm_virtual_network.main.name
|
||||
address_prefixes = ["10.0.100.0/24"]
|
||||
}
|
||||
|
||||
# NSG for private subnet
|
||||
resource "azurerm_network_security_group" "private" {
|
||||
name = "${local.name_prefix}-private-nsg"
|
||||
location = azurerm_resource_group.main.location
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
|
||||
security_rule {
|
||||
name = "allow-app-port"
|
||||
priority = 100
|
||||
direction = "Inbound"
|
||||
access = "Allow"
|
||||
protocol = "Tcp"
|
||||
source_port_range = "*"
|
||||
destination_port_range = tostring(var.app_port)
|
||||
source_address_prefix = var.vpc_cidr
|
||||
destination_address_prefix = "*"
|
||||
}
|
||||
|
||||
security_rule {
|
||||
name = "allow-mcp-port"
|
||||
priority = 110
|
||||
direction = "Inbound"
|
||||
access = "Allow"
|
||||
protocol = "Tcp"
|
||||
source_port_range = "*"
|
||||
destination_port_range = tostring(var.mcp_port)
|
||||
source_address_prefix = var.vpc_cidr
|
||||
destination_address_prefix = "*"
|
||||
}
|
||||
|
||||
security_rule {
|
||||
name = "allow-smb"
|
||||
priority = 120
|
||||
direction = "Inbound"
|
||||
access = "Allow"
|
||||
protocol = "Tcp"
|
||||
source_port_range = "*"
|
||||
destination_port_range = "445"
|
||||
source_address_prefix = var.vpc_cidr
|
||||
destination_address_prefix = "*"
|
||||
}
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "azurerm_subnet_network_security_group_association" "private" {
|
||||
subnet_id = azurerm_subnet.private.id
|
||||
network_security_group_id = azurerm_network_security_group.private.id
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Azure Files (persistent storage for SQLite)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "azurerm_storage_account" "main" {
|
||||
name = local.storage_account_name
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
location = azurerm_resource_group.main.location
|
||||
account_tier = var.environment == "production" ? "Premium" : "Standard"
|
||||
account_replication_type = var.environment == "production" ? "ZRS" : "LRS"
|
||||
account_kind = var.environment == "production" ? "FileStorage" : "StorageV2"
|
||||
|
||||
min_tls_version = "TLS1_2"
|
||||
|
||||
network_rules {
|
||||
default_action = "Deny"
|
||||
virtual_network_subnet_ids = [azurerm_subnet.private.id]
|
||||
}
|
||||
|
||||
tags = local.common_tags
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_storage_share" "appdata" {
|
||||
name = "appdata"
|
||||
storage_account_name = azurerm_storage_account.main.name
|
||||
quota = var.storage_size_gb
|
||||
access_tier = var.environment == "production" ? "Premium" : "Hot"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Key Vault (for secrets management)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "azurerm_key_vault" "main" {
|
||||
name = substr("${local.name_prefix}-kv", 0, 24)
|
||||
location = azurerm_resource_group.main.location
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
tenant_id = data.azurerm_client_config.current.tenant_id
|
||||
sku_name = "standard"
|
||||
purge_protection_enabled = var.environment == "production"
|
||||
|
||||
access_policy {
|
||||
tenant_id = data.azurerm_client_config.current.tenant_id
|
||||
object_id = data.azurerm_client_config.current.object_id
|
||||
|
||||
secret_permissions = [
|
||||
"Get", "List", "Set", "Delete", "Purge",
|
||||
]
|
||||
}
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Container Instances (Blue / Green)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "azurerm_container_group" "blue" {
|
||||
name = "${local.name_prefix}-blue"
|
||||
location = azurerm_resource_group.main.location
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
os_type = "Linux"
|
||||
ip_address_type = "Private"
|
||||
subnet_ids = [azurerm_subnet.private.id]
|
||||
restart_policy = "Always"
|
||||
|
||||
container {
|
||||
name = "app"
|
||||
image = var.app_container_image
|
||||
cpu = var.cpu / 1000.0
|
||||
memory = var.memory / 1024.0
|
||||
|
||||
ports {
|
||||
port = var.app_port
|
||||
protocol = "TCP"
|
||||
}
|
||||
|
||||
dynamic "environment_variables" {
|
||||
for_each = var.environment_variables
|
||||
content {
|
||||
name = environment_variables.key
|
||||
value = environment_variables.value
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "app-data"
|
||||
mount_path = "/app/data"
|
||||
read_only = false
|
||||
storage_account_name = azurerm_storage_account.main.name
|
||||
storage_account_key = azurerm_storage_account.main.primary_access_key
|
||||
share_name = azurerm_storage_share.appdata.name
|
||||
}
|
||||
|
||||
liveness_probe {
|
||||
http_get {
|
||||
path = var.health_check_path
|
||||
port = var.app_port
|
||||
scheme = "Http"
|
||||
}
|
||||
initial_delay_seconds = 30
|
||||
period_seconds = 30
|
||||
failure_threshold = 3
|
||||
}
|
||||
|
||||
readiness_probe {
|
||||
http_get {
|
||||
path = var.health_check_path
|
||||
port = var.app_port
|
||||
scheme = "Http"
|
||||
}
|
||||
initial_delay_seconds = 10
|
||||
period_seconds = 10
|
||||
failure_threshold = 3
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "container" {
|
||||
for_each = var.mcp_container_image != "" ? [1] : []
|
||||
content {
|
||||
name = "mcp-sidecar"
|
||||
image = var.mcp_container_image
|
||||
cpu = 0.25
|
||||
memory = 0.25
|
||||
|
||||
ports {
|
||||
port = var.mcp_port
|
||||
protocol = "TCP"
|
||||
}
|
||||
|
||||
environment_variables = {
|
||||
NODE_ENV = "production"
|
||||
MCP_PORT = tostring(var.mcp_port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
deployment_slot = "blue"
|
||||
})
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
tags["last_deployed"],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_container_group" "green" {
|
||||
count = var.active_deployment_slot == "green" || var.green_weight > 0 ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-green"
|
||||
location = azurerm_resource_group.main.location
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
os_type = "Linux"
|
||||
ip_address_type = "Private"
|
||||
subnet_ids = [azurerm_subnet.private.id]
|
||||
restart_policy = "Always"
|
||||
|
||||
container {
|
||||
name = "app"
|
||||
image = var.app_container_image
|
||||
cpu = var.cpu / 1000.0
|
||||
memory = var.memory / 1024.0
|
||||
|
||||
ports {
|
||||
port = var.app_port
|
||||
protocol = "TCP"
|
||||
}
|
||||
|
||||
dynamic "environment_variables" {
|
||||
for_each = var.environment_variables
|
||||
content {
|
||||
name = environment_variables.key
|
||||
value = environment_variables.value
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "app-data"
|
||||
mount_path = "/app/data"
|
||||
read_only = false
|
||||
storage_account_name = azurerm_storage_account.main.name
|
||||
storage_account_key = azurerm_storage_account.main.primary_access_key
|
||||
share_name = azurerm_storage_share.appdata.name
|
||||
}
|
||||
|
||||
liveness_probe {
|
||||
http_get {
|
||||
path = var.health_check_path
|
||||
port = var.app_port
|
||||
scheme = "Http"
|
||||
}
|
||||
initial_delay_seconds = 30
|
||||
period_seconds = 30
|
||||
failure_threshold = 3
|
||||
}
|
||||
}
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
deployment_slot = "green"
|
||||
})
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Application Gateway (L7 load balancer with WebSocket + SSL)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "azurerm_public_ip" "appgw" {
|
||||
name = "${local.name_prefix}-appgw-pip"
|
||||
location = azurerm_resource_group.main.location
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
allocation_method = "Static"
|
||||
sku = "Standard"
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "azurerm_application_gateway" "main" {
|
||||
name = "${local.name_prefix}-appgw"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
location = azurerm_resource_group.main.location
|
||||
enable_http2 = true
|
||||
|
||||
sku {
|
||||
name = var.environment == "production" ? "WAF_v2" : "Standard_v2"
|
||||
tier = var.environment == "production" ? "WAF_v2" : "Standard_v2"
|
||||
capacity = var.environment == "production" ? 2 : 1
|
||||
}
|
||||
|
||||
gateway_ip_configuration {
|
||||
name = "gateway-ip"
|
||||
subnet_id = azurerm_subnet.appgw.id
|
||||
}
|
||||
|
||||
frontend_ip_configuration {
|
||||
name = "frontend-ip"
|
||||
public_ip_address_id = azurerm_public_ip.appgw.id
|
||||
}
|
||||
|
||||
frontend_port {
|
||||
name = "http"
|
||||
port = 80
|
||||
}
|
||||
|
||||
frontend_port {
|
||||
name = "https"
|
||||
port = 443
|
||||
}
|
||||
|
||||
# Blue backend pool
|
||||
backend_address_pool {
|
||||
name = "blue-pool"
|
||||
ip_addresses = [azurerm_container_group.blue.ip_address]
|
||||
}
|
||||
|
||||
# Green backend pool
|
||||
dynamic "backend_address_pool" {
|
||||
for_each = length(azurerm_container_group.green) > 0 ? [1] : []
|
||||
content {
|
||||
name = "green-pool"
|
||||
ip_addresses = [azurerm_container_group.green[0].ip_address]
|
||||
}
|
||||
}
|
||||
|
||||
backend_http_settings {
|
||||
name = "app-settings"
|
||||
cookie_based_affinity = "Enabled"
|
||||
port = var.app_port
|
||||
protocol = "Http"
|
||||
request_timeout = 300 # WebSocket support
|
||||
pick_host_name_from_backend_address = false
|
||||
|
||||
connection_draining {
|
||||
enabled = true
|
||||
drain_timeout_sec = 60
|
||||
}
|
||||
|
||||
probe_name = "app-health"
|
||||
}
|
||||
|
||||
probe {
|
||||
name = "app-health"
|
||||
protocol = "Http"
|
||||
path = var.health_check_path
|
||||
host = "127.0.0.1"
|
||||
interval = var.health_check_interval
|
||||
timeout = var.health_check_timeout
|
||||
unhealthy_threshold = var.health_check_unhealthy_threshold
|
||||
|
||||
match {
|
||||
status_code = ["200"]
|
||||
}
|
||||
}
|
||||
|
||||
# HTTP listener
|
||||
http_listener {
|
||||
name = "http-listener"
|
||||
frontend_ip_configuration_name = "frontend-ip"
|
||||
frontend_port_name = "http"
|
||||
protocol = "Http"
|
||||
}
|
||||
|
||||
# Routing rule – HTTP to blue pool
|
||||
request_routing_rule {
|
||||
name = "http-routing"
|
||||
priority = 100
|
||||
rule_type = "Basic"
|
||||
http_listener_name = "http-listener"
|
||||
backend_address_pool_name = var.active_deployment_slot == "blue" ? "blue-pool" : "green-pool"
|
||||
backend_http_settings_name = "app-settings"
|
||||
}
|
||||
|
||||
tags = local.common_tags
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = false
|
||||
ignore_changes = [
|
||||
tags["last_deployed"],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Azure Monitor (alerts and diagnostics)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "azurerm_monitor_action_group" "main" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-alerts"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
short_name = substr(local.name_prefix, 0, 12)
|
||||
|
||||
dynamic "email_receiver" {
|
||||
for_each = var.alert_email != "" ? [1] : []
|
||||
content {
|
||||
name = "email-alert"
|
||||
email_address = var.alert_email
|
||||
}
|
||||
}
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "azurerm_monitor_metric_alert" "appgw_unhealthy" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-unhealthy-backend"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
scopes = [azurerm_application_gateway.main.id]
|
||||
description = "Alert when backend health drops below threshold"
|
||||
severity = 1
|
||||
|
||||
criteria {
|
||||
metric_namespace = "Microsoft.Network/applicationGateways"
|
||||
metric_name = "UnhealthyHostCount"
|
||||
aggregation = "Average"
|
||||
operator = "GreaterThan"
|
||||
threshold = 0
|
||||
}
|
||||
|
||||
action {
|
||||
action_group_id = azurerm_monitor_action_group.main[0].id
|
||||
}
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "azurerm_monitor_metric_alert" "appgw_5xx" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-high-5xx"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
scopes = [azurerm_application_gateway.main.id]
|
||||
description = "High 5xx error rate on Application Gateway"
|
||||
severity = 2
|
||||
|
||||
criteria {
|
||||
metric_namespace = "Microsoft.Network/applicationGateways"
|
||||
metric_name = "ResponseStatus"
|
||||
aggregation = "Count"
|
||||
operator = "GreaterThan"
|
||||
threshold = 10
|
||||
|
||||
dimension {
|
||||
name = "HttpStatusGroup"
|
||||
operator = "Include"
|
||||
values = ["5xx"]
|
||||
}
|
||||
}
|
||||
|
||||
action {
|
||||
action_group_id = azurerm_monitor_action_group.main[0].id
|
||||
}
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "azurerm_monitor_metric_alert" "appgw_latency" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-high-latency"
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
scopes = [azurerm_application_gateway.main.id]
|
||||
description = "High backend response latency"
|
||||
severity = 2
|
||||
|
||||
criteria {
|
||||
metric_namespace = "Microsoft.Network/applicationGateways"
|
||||
metric_name = "BackendLastByteResponseTime"
|
||||
aggregation = "Average"
|
||||
operator = "GreaterThan"
|
||||
threshold = 2000 # ms
|
||||
}
|
||||
|
||||
action {
|
||||
action_group_id = azurerm_monitor_action_group.main[0].id
|
||||
}
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# Log Analytics Workspace
|
||||
resource "azurerm_log_analytics_workspace" "main" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-logs"
|
||||
location = azurerm_resource_group.main.location
|
||||
resource_group_name = azurerm_resource_group.main.name
|
||||
sku = "PerGB2018"
|
||||
retention_in_days = var.log_retention_days
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# Diagnostic settings for App Gateway
|
||||
resource "azurerm_monitor_diagnostic_setting" "appgw" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-appgw-diag"
|
||||
target_resource_id = azurerm_application_gateway.main.id
|
||||
log_analytics_workspace_id = azurerm_log_analytics_workspace.main[0].id
|
||||
|
||||
enabled_log {
|
||||
category = "ApplicationGatewayAccessLog"
|
||||
}
|
||||
|
||||
enabled_log {
|
||||
category = "ApplicationGatewayPerformanceLog"
|
||||
}
|
||||
|
||||
metric {
|
||||
category = "AllMetrics"
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Azure provider outputs
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
output "application_url" {
|
||||
description = "Public URL of the Claude Code Agent Monitor dashboard"
|
||||
value = "http://${azurerm_public_ip.appgw.ip_address}"
|
||||
}
|
||||
|
||||
output "public_ip" {
|
||||
description = "Public IP address of the Application Gateway"
|
||||
value = azurerm_public_ip.appgw.ip_address
|
||||
}
|
||||
|
||||
output "resource_group_name" {
|
||||
description = "Name of the Azure resource group"
|
||||
value = azurerm_resource_group.main.name
|
||||
}
|
||||
|
||||
output "vnet_id" {
|
||||
description = "ID of the Virtual Network"
|
||||
value = azurerm_virtual_network.main.id
|
||||
}
|
||||
|
||||
output "blue_container_group_id" {
|
||||
description = "ID of the blue container group"
|
||||
value = azurerm_container_group.blue.id
|
||||
}
|
||||
|
||||
output "green_container_group_id" {
|
||||
description = "ID of the green container group (if deployed)"
|
||||
value = length(azurerm_container_group.green) > 0 ? azurerm_container_group.green[0].id : ""
|
||||
}
|
||||
|
||||
output "storage_account_name" {
|
||||
description = "Name of the Azure Storage Account"
|
||||
value = azurerm_storage_account.main.name
|
||||
}
|
||||
|
||||
output "key_vault_uri" {
|
||||
description = "URI of the Azure Key Vault"
|
||||
value = azurerm_key_vault.main.vault_uri
|
||||
}
|
||||
|
||||
output "app_gateway_id" {
|
||||
description = "ID of the Application Gateway"
|
||||
value = azurerm_application_gateway.main.id
|
||||
}
|
||||
|
||||
output "log_analytics_workspace_id" {
|
||||
description = "ID of the Log Analytics workspace (if monitoring enabled)"
|
||||
value = var.enable_monitoring ? azurerm_log_analytics_workspace.main[0].id : ""
|
||||
}
|
||||
|
||||
output "region" {
|
||||
description = "Azure region"
|
||||
value = var.region
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Azure provider – Terraform and provider constraints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
azurerm = {
|
||||
source = "hashicorp/azurerm"
|
||||
version = "~> 3.80"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Azure provider variables
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project identifier used in resource naming"
|
||||
type = string
|
||||
default = "claude-agent-monitor"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment: dev, staging, or production"
|
||||
type = string
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "production"], var.environment)
|
||||
error_message = "environment must be one of: dev, staging, production."
|
||||
}
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "Azure region for resource deployment"
|
||||
type = string
|
||||
default = "eastus"
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Additional tags to apply to all resources"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "vpc_cidr" {
|
||||
description = "Address space for the Virtual Network"
|
||||
type = string
|
||||
default = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
variable "public_subnet_cidrs" {
|
||||
description = "Address prefixes for the public subnet"
|
||||
type = list(string)
|
||||
default = ["10.0.1.0/24"]
|
||||
}
|
||||
|
||||
variable "private_subnet_cidrs" {
|
||||
description = "Address prefixes for the private subnet"
|
||||
type = list(string)
|
||||
default = ["10.0.11.0/24"]
|
||||
}
|
||||
|
||||
# ── Compute ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_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 = "Application container port"
|
||||
type = number
|
||||
default = 4820
|
||||
}
|
||||
|
||||
variable "mcp_port" {
|
||||
description = "MCP sidecar container port"
|
||||
type = number
|
||||
default = 8819
|
||||
}
|
||||
|
||||
variable "cpu" {
|
||||
description = "CPU millicores for container instances"
|
||||
type = number
|
||||
default = 512
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory in MiB for container instances"
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "environment_variables" {
|
||||
description = "Environment variables for the application container"
|
||||
type = map(string)
|
||||
default = {
|
||||
NODE_ENV = "production"
|
||||
DASHBOARD_PORT = "4820"
|
||||
}
|
||||
}
|
||||
|
||||
# ── Deployment ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "active_deployment_slot" {
|
||||
description = "Active deployment slot: blue or green"
|
||||
type = string
|
||||
default = "blue"
|
||||
validation {
|
||||
condition = contains(["blue", "green"], var.active_deployment_slot)
|
||||
error_message = "active_deployment_slot must be blue or green."
|
||||
}
|
||||
}
|
||||
|
||||
variable "blue_weight" {
|
||||
description = "Traffic weight for blue backend (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 backend (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."
|
||||
}
|
||||
}
|
||||
|
||||
# ── TLS / Domain ────────────────────────────────────────────────────────────
|
||||
|
||||
variable "domain_name" {
|
||||
description = "FQDN for the application (empty to skip)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ── Storage ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "storage_size_gb" {
|
||||
description = "Azure Files share quota in GiB"
|
||||
type = number
|
||||
default = 20
|
||||
}
|
||||
|
||||
# ── Health check ────────────────────────────────────────────────────────────
|
||||
|
||||
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_unhealthy_threshold" {
|
||||
description = "Consecutive failures to mark unhealthy"
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
# ── Monitoring ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "enable_monitoring" {
|
||||
description = "Enable Azure Monitor alerts and diagnostics"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "alert_email" {
|
||||
description = "Email address for Azure Monitor alerts"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "log_retention_days" {
|
||||
description = "Log Analytics workspace retention in days"
|
||||
type = number
|
||||
default = 30
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GCP Provider – Full implementation for Claude Code Agent Monitor
|
||||
#
|
||||
# Architecture:
|
||||
# VPC → Cloud Run (blue/green) → Filestore → Cloud Load Balancer
|
||||
# → Cloud Monitoring → Managed SSL Certificate
|
||||
#
|
||||
# Cloud Run is chosen over GKE for cost efficiency and operational simplicity
|
||||
# for this containerised workload. Filestore provides NFS for SQLite.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
provider "google" {
|
||||
project = var.gcp_project_id
|
||||
region = var.region
|
||||
}
|
||||
|
||||
provider "google-beta" {
|
||||
project = var.gcp_project_id
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# ── Locals ──────────────────────────────────────────────────────────────────
|
||||
|
||||
locals {
|
||||
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
|
||||
|
||||
common_labels = merge(
|
||||
{
|
||||
project = replace(var.project_name, "-", "_")
|
||||
environment = var.environment
|
||||
managed_by = "terraform"
|
||||
cloud_provider = "gcp"
|
||||
},
|
||||
{ for k, v in var.tags : replace(k, "-", "_") => replace(v, "-", "_") },
|
||||
)
|
||||
}
|
||||
|
||||
# ── Enable required APIs ───────────────────────────────────────────────────
|
||||
|
||||
resource "google_project_service" "apis" {
|
||||
for_each = toset([
|
||||
"run.googleapis.com",
|
||||
"compute.googleapis.com",
|
||||
"file.googleapis.com",
|
||||
"vpcaccess.googleapis.com",
|
||||
"monitoring.googleapis.com",
|
||||
"logging.googleapis.com",
|
||||
"certificatemanager.googleapis.com",
|
||||
])
|
||||
|
||||
service = each.value
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# VPC Network
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "google_compute_network" "main" {
|
||||
name = "${local.name_prefix}-vpc"
|
||||
auto_create_subnetworks = false
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "private" {
|
||||
name = "${local.name_prefix}-private"
|
||||
ip_cidr_range = var.private_subnet_cidrs[0]
|
||||
region = var.region
|
||||
network = google_compute_network.main.id
|
||||
|
||||
private_ip_google_access = true
|
||||
|
||||
log_config {
|
||||
aggregation_interval = "INTERVAL_5_SEC"
|
||||
flow_sampling = 0.5
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "proxy" {
|
||||
name = "${local.name_prefix}-proxy"
|
||||
ip_cidr_range = "10.0.100.0/24"
|
||||
region = var.region
|
||||
network = google_compute_network.main.id
|
||||
purpose = "REGIONAL_MANAGED_PROXY"
|
||||
role = "ACTIVE"
|
||||
}
|
||||
|
||||
# Cloud NAT for outbound internet
|
||||
resource "google_compute_router" "main" {
|
||||
name = "${local.name_prefix}-router"
|
||||
region = var.region
|
||||
network = google_compute_network.main.id
|
||||
}
|
||||
|
||||
resource "google_compute_router_nat" "main" {
|
||||
name = "${local.name_prefix}-nat"
|
||||
router = google_compute_router.main.name
|
||||
region = var.region
|
||||
nat_ip_allocate_option = "AUTO_ONLY"
|
||||
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
|
||||
|
||||
log_config {
|
||||
enable = true
|
||||
filter = "ERRORS_ONLY"
|
||||
}
|
||||
}
|
||||
|
||||
# VPC Connector for Cloud Run → Filestore
|
||||
resource "google_vpc_access_connector" "main" {
|
||||
name = "${local.name_prefix}-conn"
|
||||
region = var.region
|
||||
network = google_compute_network.main.id
|
||||
ip_cidr_range = "10.0.200.0/28"
|
||||
min_instances = 2
|
||||
max_instances = var.environment == "production" ? 10 : 3
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
# Firewall rules
|
||||
resource "google_compute_firewall" "allow_health_checks" {
|
||||
name = "${local.name_prefix}-allow-health-checks"
|
||||
network = google_compute_network.main.id
|
||||
|
||||
allow {
|
||||
protocol = "tcp"
|
||||
ports = [tostring(var.app_port), tostring(var.mcp_port)]
|
||||
}
|
||||
|
||||
source_ranges = ["130.211.0.0/22", "35.191.0.0/16"] # GCP health check ranges
|
||||
target_tags = ["${local.name_prefix}-app"]
|
||||
}
|
||||
|
||||
resource "google_compute_firewall" "allow_internal" {
|
||||
name = "${local.name_prefix}-allow-internal"
|
||||
network = google_compute_network.main.id
|
||||
|
||||
allow {
|
||||
protocol = "tcp"
|
||||
ports = [tostring(var.app_port), tostring(var.mcp_port), "2049"]
|
||||
}
|
||||
|
||||
source_ranges = [var.vpc_cidr]
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Filestore (NFS for SQLite persistence)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "google_filestore_instance" "main" {
|
||||
name = "${local.name_prefix}-data"
|
||||
location = "${var.region}-b"
|
||||
tier = var.environment == "production" ? "BASIC_SSD" : "BASIC_HDD"
|
||||
|
||||
file_shares {
|
||||
name = "appdata"
|
||||
capacity_gb = var.storage_size_gb
|
||||
}
|
||||
|
||||
networks {
|
||||
network = google_compute_network.main.name
|
||||
modes = ["MODE_IPV4"]
|
||||
}
|
||||
|
||||
labels = local.common_labels
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Cloud Run services (Blue / Green)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "google_cloud_run_v2_service" "blue" {
|
||||
name = "${local.name_prefix}-blue"
|
||||
location = var.region
|
||||
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
|
||||
|
||||
template {
|
||||
scaling {
|
||||
min_instance_count = var.active_deployment_slot == "blue" ? var.min_replicas : 0
|
||||
max_instance_count = var.active_deployment_slot == "blue" ? var.max_replicas : 1
|
||||
}
|
||||
|
||||
vpc_access {
|
||||
connector = google_vpc_access_connector.main.id
|
||||
egress = "ALL_TRAFFIC"
|
||||
}
|
||||
|
||||
containers {
|
||||
image = var.app_container_image
|
||||
name = "app"
|
||||
|
||||
ports {
|
||||
container_port = var.app_port
|
||||
}
|
||||
|
||||
resources {
|
||||
limits = {
|
||||
cpu = "${var.cpu}m"
|
||||
memory = "${var.memory}Mi"
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "env" {
|
||||
for_each = var.environment_variables
|
||||
content {
|
||||
name = env.key
|
||||
value = env.value
|
||||
}
|
||||
}
|
||||
|
||||
env {
|
||||
name = "FILESTORE_IP"
|
||||
value = google_filestore_instance.main.networks[0].ip_addresses[0]
|
||||
}
|
||||
|
||||
startup_probe {
|
||||
http_get {
|
||||
path = var.health_check_path
|
||||
port = var.app_port
|
||||
}
|
||||
initial_delay_seconds = 10
|
||||
period_seconds = 10
|
||||
failure_threshold = 5
|
||||
}
|
||||
|
||||
liveness_probe {
|
||||
http_get {
|
||||
path = var.health_check_path
|
||||
port = var.app_port
|
||||
}
|
||||
period_seconds = 30
|
||||
failure_threshold = 3
|
||||
}
|
||||
|
||||
volume_mounts {
|
||||
name = "app-data"
|
||||
mount_path = "/app/data"
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "containers" {
|
||||
for_each = var.mcp_container_image != "" ? [1] : []
|
||||
content {
|
||||
image = var.mcp_container_image
|
||||
name = "mcp-sidecar"
|
||||
|
||||
ports {
|
||||
container_port = var.mcp_port
|
||||
}
|
||||
|
||||
resources {
|
||||
limits = {
|
||||
cpu = "250m"
|
||||
memory = "256Mi"
|
||||
}
|
||||
}
|
||||
|
||||
env {
|
||||
name = "NODE_ENV"
|
||||
value = "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volumes {
|
||||
name = "app-data"
|
||||
nfs {
|
||||
server = google_filestore_instance.main.networks[0].ip_addresses[0]
|
||||
path = "/appdata"
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
session_affinity = true
|
||||
timeout = "300s"
|
||||
}
|
||||
|
||||
labels = local.common_labels
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
client,
|
||||
client_version,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service" "green" {
|
||||
name = "${local.name_prefix}-green"
|
||||
location = var.region
|
||||
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
|
||||
|
||||
template {
|
||||
scaling {
|
||||
min_instance_count = var.active_deployment_slot == "green" ? var.min_replicas : 0
|
||||
max_instance_count = var.active_deployment_slot == "green" ? var.max_replicas : 1
|
||||
}
|
||||
|
||||
vpc_access {
|
||||
connector = google_vpc_access_connector.main.id
|
||||
egress = "ALL_TRAFFIC"
|
||||
}
|
||||
|
||||
containers {
|
||||
image = var.app_container_image
|
||||
name = "app"
|
||||
|
||||
ports {
|
||||
container_port = var.app_port
|
||||
}
|
||||
|
||||
resources {
|
||||
limits = {
|
||||
cpu = "${var.cpu}m"
|
||||
memory = "${var.memory}Mi"
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "env" {
|
||||
for_each = var.environment_variables
|
||||
content {
|
||||
name = env.key
|
||||
value = env.value
|
||||
}
|
||||
}
|
||||
|
||||
env {
|
||||
name = "FILESTORE_IP"
|
||||
value = google_filestore_instance.main.networks[0].ip_addresses[0]
|
||||
}
|
||||
|
||||
startup_probe {
|
||||
http_get {
|
||||
path = var.health_check_path
|
||||
port = var.app_port
|
||||
}
|
||||
initial_delay_seconds = 10
|
||||
period_seconds = 10
|
||||
failure_threshold = 5
|
||||
}
|
||||
|
||||
liveness_probe {
|
||||
http_get {
|
||||
path = var.health_check_path
|
||||
port = var.app_port
|
||||
}
|
||||
period_seconds = 30
|
||||
failure_threshold = 3
|
||||
}
|
||||
|
||||
volume_mounts {
|
||||
name = "app-data"
|
||||
mount_path = "/app/data"
|
||||
}
|
||||
}
|
||||
|
||||
volumes {
|
||||
name = "app-data"
|
||||
nfs {
|
||||
server = google_filestore_instance.main.networks[0].ip_addresses[0]
|
||||
path = "/appdata"
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
session_affinity = true
|
||||
timeout = "300s"
|
||||
}
|
||||
|
||||
labels = local.common_labels
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
client,
|
||||
client_version,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# IAM – restrict access to load balancer service account only
|
||||
# Cloud Run ingress is set to INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER,
|
||||
# so public IAM bindings are not needed. The LB routes traffic internally.
|
||||
# To grant specific service account access, replace with:
|
||||
# member = "serviceAccount:<your-lb-service-account>@<project>.iam.gserviceaccount.com"
|
||||
#
|
||||
# resource "google_cloud_run_v2_service_iam_member" "blue_invoker" {
|
||||
# name = google_cloud_run_v2_service.blue.name
|
||||
# location = var.region
|
||||
# role = "roles/run.invoker"
|
||||
# member = "serviceAccount:${var.project_id}-compute@developer.gserviceaccount.com"
|
||||
# }
|
||||
#
|
||||
# resource "google_cloud_run_v2_service_iam_member" "green_invoker" {
|
||||
# name = google_cloud_run_v2_service.green.name
|
||||
# location = var.region
|
||||
# role = "roles/run.invoker"
|
||||
# member = "serviceAccount:${var.project_id}-compute@developer.gserviceaccount.com"
|
||||
# }
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# External Application Load Balancer
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Serverless NEGs for Cloud Run
|
||||
resource "google_compute_region_network_endpoint_group" "blue" {
|
||||
name = "${local.name_prefix}-blue-neg"
|
||||
region = var.region
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.blue.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_region_network_endpoint_group" "green" {
|
||||
name = "${local.name_prefix}-green-neg"
|
||||
region = var.region
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.green.name
|
||||
}
|
||||
}
|
||||
|
||||
# Backend service with weighted backends for blue/green
|
||||
resource "google_compute_backend_service" "main" {
|
||||
name = "${local.name_prefix}-backend"
|
||||
protocol = "HTTP"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
timeout_sec = 300 # WebSocket support
|
||||
|
||||
session_affinity = "GENERATED_COOKIE"
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.blue.id
|
||||
capacity_scaler = var.blue_weight / 100
|
||||
}
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.green.id
|
||||
capacity_scaler = var.green_weight / 100
|
||||
}
|
||||
|
||||
health_checks = [google_compute_health_check.main.id]
|
||||
|
||||
log_config {
|
||||
enable = true
|
||||
sample_rate = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_health_check" "main" {
|
||||
name = "${local.name_prefix}-hc"
|
||||
|
||||
http_health_check {
|
||||
port = var.app_port
|
||||
request_path = var.health_check_path
|
||||
}
|
||||
|
||||
check_interval_sec = var.health_check_interval
|
||||
timeout_sec = var.health_check_timeout
|
||||
healthy_threshold = var.health_check_healthy_threshold
|
||||
unhealthy_threshold = var.health_check_unhealthy_threshold
|
||||
}
|
||||
|
||||
# URL map
|
||||
resource "google_compute_url_map" "main" {
|
||||
name = "${local.name_prefix}-urlmap"
|
||||
default_service = google_compute_backend_service.main.id
|
||||
}
|
||||
|
||||
# Managed SSL certificate (optional)
|
||||
resource "google_compute_managed_ssl_certificate" "main" {
|
||||
count = var.domain_name != "" ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-cert"
|
||||
|
||||
managed {
|
||||
domains = [var.domain_name]
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS proxy
|
||||
resource "google_compute_target_https_proxy" "main" {
|
||||
count = var.domain_name != "" ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-https-proxy"
|
||||
url_map = google_compute_url_map.main.id
|
||||
ssl_certificates = [google_compute_managed_ssl_certificate.main[0].id]
|
||||
}
|
||||
|
||||
# HTTP proxy (for redirect or direct access)
|
||||
resource "google_compute_target_http_proxy" "main" {
|
||||
name = "${local.name_prefix}-http-proxy"
|
||||
url_map = google_compute_url_map.main.id
|
||||
}
|
||||
|
||||
# Global forwarding rules
|
||||
resource "google_compute_global_forwarding_rule" "https" {
|
||||
count = var.domain_name != "" ? 1 : 0
|
||||
|
||||
name = "${local.name_prefix}-https"
|
||||
target = google_compute_target_https_proxy.main[0].id
|
||||
port_range = "443"
|
||||
ip_protocol = "TCP"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
}
|
||||
|
||||
resource "google_compute_global_forwarding_rule" "http" {
|
||||
name = "${local.name_prefix}-http"
|
||||
target = google_compute_target_http_proxy.main.id
|
||||
port_range = "80"
|
||||
ip_protocol = "TCP"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Cloud Monitoring – Alert policies
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "google_monitoring_notification_channel" "email" {
|
||||
count = var.alert_email != "" ? 1 : 0
|
||||
|
||||
display_name = "${local.name_prefix}-email"
|
||||
type = "email"
|
||||
|
||||
labels = {
|
||||
email_address = var.alert_email
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_monitoring_alert_policy" "high_latency" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
display_name = "${local.name_prefix}-high-latency"
|
||||
combiner = "OR"
|
||||
|
||||
conditions {
|
||||
display_name = "Cloud Run request latency > 2s"
|
||||
|
||||
condition_threshold {
|
||||
filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_latencies\""
|
||||
comparison = "COMPARISON_GT"
|
||||
duration = "300s"
|
||||
|
||||
threshold_value = 2000 # ms
|
||||
|
||||
aggregations {
|
||||
alignment_period = "60s"
|
||||
per_series_aligner = "ALIGN_PERCENTILE_99"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notification_channels = var.alert_email != "" ? [google_monitoring_notification_channel.email[0].id] : []
|
||||
|
||||
alert_strategy {
|
||||
auto_close = "604800s"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_monitoring_alert_policy" "high_error_rate" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
display_name = "${local.name_prefix}-high-error-rate"
|
||||
combiner = "OR"
|
||||
|
||||
conditions {
|
||||
display_name = "Cloud Run 5xx error rate"
|
||||
|
||||
condition_threshold {
|
||||
filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_count\" AND metric.labels.response_code_class=\"5xx\""
|
||||
comparison = "COMPARISON_GT"
|
||||
duration = "300s"
|
||||
|
||||
threshold_value = 10
|
||||
|
||||
aggregations {
|
||||
alignment_period = "60s"
|
||||
per_series_aligner = "ALIGN_RATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notification_channels = var.alert_email != "" ? [google_monitoring_notification_channel.email[0].id] : []
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Cloud Monitoring Dashboard
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "google_monitoring_dashboard" "main" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
dashboard_json = jsonencode({
|
||||
displayName = "${local.name_prefix} Dashboard"
|
||||
gridLayout = {
|
||||
columns = 2
|
||||
widgets = [
|
||||
{
|
||||
title = "Request Count"
|
||||
xyChart = {
|
||||
dataSets = [{
|
||||
timeSeriesQuery = {
|
||||
timeSeriesFilter = {
|
||||
filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_count\""
|
||||
aggregation = {
|
||||
alignmentPeriod = "60s"
|
||||
perSeriesAligner = "ALIGN_RATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
title = "Request Latency (p99)"
|
||||
xyChart = {
|
||||
dataSets = [{
|
||||
timeSeriesQuery = {
|
||||
timeSeriesFilter = {
|
||||
filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_latencies\""
|
||||
aggregation = {
|
||||
alignmentPeriod = "60s"
|
||||
perSeriesAligner = "ALIGN_PERCENTILE_99"
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
title = "Instance Count"
|
||||
xyChart = {
|
||||
dataSets = [{
|
||||
timeSeriesQuery = {
|
||||
timeSeriesFilter = {
|
||||
filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/container/instance_count\""
|
||||
aggregation = {
|
||||
alignmentPeriod = "60s"
|
||||
perSeriesAligner = "ALIGN_MEAN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
title = "CPU Utilization"
|
||||
xyChart = {
|
||||
dataSets = [{
|
||||
timeSeriesQuery = {
|
||||
timeSeriesFilter = {
|
||||
filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/container/cpu/utilizations\""
|
||||
aggregation = {
|
||||
alignmentPeriod = "60s"
|
||||
perSeriesAligner = "ALIGN_PERCENTILE_99"
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GCP provider outputs
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
output "application_url" {
|
||||
description = "Public URL of the Claude Code Agent Monitor dashboard"
|
||||
value = var.domain_name != "" ? "https://${var.domain_name}" : "http://${google_compute_global_forwarding_rule.http.ip_address}"
|
||||
}
|
||||
|
||||
output "load_balancer_ip" {
|
||||
description = "External IP address of the load balancer"
|
||||
value = google_compute_global_forwarding_rule.http.ip_address
|
||||
}
|
||||
|
||||
output "blue_service_url" {
|
||||
description = "URL of the blue Cloud Run service"
|
||||
value = google_cloud_run_v2_service.blue.uri
|
||||
}
|
||||
|
||||
output "green_service_url" {
|
||||
description = "URL of the green Cloud Run service"
|
||||
value = google_cloud_run_v2_service.green.uri
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "Self-link of the VPC network"
|
||||
value = google_compute_network.main.self_link
|
||||
}
|
||||
|
||||
output "filestore_ip" {
|
||||
description = "IP address of the Filestore instance"
|
||||
value = google_filestore_instance.main.networks[0].ip_addresses[0]
|
||||
}
|
||||
|
||||
output "filestore_share" {
|
||||
description = "Filestore share name"
|
||||
value = google_filestore_instance.main.file_shares[0].name
|
||||
}
|
||||
|
||||
output "monitoring_dashboard_url" {
|
||||
description = "Cloud Monitoring dashboard URL"
|
||||
value = var.enable_monitoring ? "https://console.cloud.google.com/monitoring/dashboards?project=${var.gcp_project_id}" : "monitoring disabled"
|
||||
}
|
||||
|
||||
output "project_id" {
|
||||
description = "GCP project ID"
|
||||
value = var.gcp_project_id
|
||||
}
|
||||
|
||||
output "region" {
|
||||
description = "GCP region"
|
||||
value = var.region
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GCP provider – Terraform and provider constraints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
google-beta = {
|
||||
source = "hashicorp/google-beta"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GCP provider variables
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project identifier used in resource naming"
|
||||
type = string
|
||||
default = "claude-agent-monitor"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment: dev, staging, or production"
|
||||
type = string
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "production"], var.environment)
|
||||
error_message = "environment must be one of: dev, staging, production."
|
||||
}
|
||||
}
|
||||
|
||||
variable "gcp_project_id" {
|
||||
description = "GCP project ID for resource deployment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "GCP region for resource deployment"
|
||||
type = string
|
||||
default = "us-central1"
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Additional labels to apply to all resources"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "vpc_cidr" {
|
||||
description = "CIDR block for the VPC (used for firewall rules)"
|
||||
type = string
|
||||
default = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
variable "private_subnet_cidrs" {
|
||||
description = "CIDR blocks for private subnets"
|
||||
type = list(string)
|
||||
default = ["10.0.11.0/24"]
|
||||
}
|
||||
|
||||
# ── Compute ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_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 = "Application container port"
|
||||
type = number
|
||||
default = 4820
|
||||
}
|
||||
|
||||
variable "mcp_port" {
|
||||
description = "MCP sidecar container port"
|
||||
type = number
|
||||
default = 8819
|
||||
}
|
||||
|
||||
variable "cpu" {
|
||||
description = "CPU millicores for each Cloud Run instance"
|
||||
type = number
|
||||
default = 512
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory in MiB for each Cloud Run instance"
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "min_replicas" {
|
||||
description = "Minimum number of Cloud Run instances"
|
||||
type = number
|
||||
default = 0
|
||||
}
|
||||
|
||||
variable "max_replicas" {
|
||||
description = "Maximum number of Cloud Run instances"
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
variable "environment_variables" {
|
||||
description = "Environment variables for the application container"
|
||||
type = map(string)
|
||||
default = {
|
||||
NODE_ENV = "production"
|
||||
DASHBOARD_PORT = "4820"
|
||||
}
|
||||
}
|
||||
|
||||
# ── Deployment ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "active_deployment_slot" {
|
||||
description = "Active deployment slot: blue or green"
|
||||
type = string
|
||||
default = "blue"
|
||||
validation {
|
||||
condition = contains(["blue", "green"], var.active_deployment_slot)
|
||||
error_message = "active_deployment_slot must be blue or green."
|
||||
}
|
||||
}
|
||||
|
||||
variable "blue_weight" {
|
||||
description = "Traffic weight for blue service (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 service (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."
|
||||
}
|
||||
}
|
||||
|
||||
# ── TLS / Domain ────────────────────────────────────────────────────────────
|
||||
|
||||
variable "domain_name" {
|
||||
description = "FQDN for managed SSL certificate (empty for HTTP only)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ── Storage ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "storage_size_gb" {
|
||||
description = "Filestore capacity in GiB"
|
||||
type = number
|
||||
default = 1024 # Filestore minimum for BASIC_HDD
|
||||
}
|
||||
|
||||
# ── Health check ────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# ── Monitoring ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "enable_monitoring" {
|
||||
description = "Enable Cloud Monitoring alerts and dashboard"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "alert_email" {
|
||||
description = "Email for monitoring notifications"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# OCI Provider – Full implementation for Claude Code Agent Monitor
|
||||
#
|
||||
# Architecture:
|
||||
# VCN → Container Instances (blue/green) → File Storage Service
|
||||
# → Flexible Load Balancer → OCI Monitoring & Notifications
|
||||
#
|
||||
# OCI Container Instances provide a serverless container runtime.
|
||||
# File Storage Service (FSS) delivers NFS for SQLite persistence.
|
||||
# Flexible Load Balancer supports WebSocket, SSL, and weighted backends.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
provider "oci" {
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# ── Data sources ────────────────────────────────────────────────────────────
|
||||
|
||||
data "oci_identity_availability_domains" "ads" {
|
||||
compartment_id = var.compartment_id
|
||||
}
|
||||
|
||||
data "oci_identity_tenancy" "current" {
|
||||
tenancy_id = var.tenancy_id
|
||||
}
|
||||
|
||||
# ── Locals ──────────────────────────────────────────────────────────────────
|
||||
|
||||
locals {
|
||||
name_prefix = lower(replace("${var.project_name}-${var.environment}", "_", "-"))
|
||||
ad_name = data.oci_identity_availability_domains.ads.availability_domains[0].name
|
||||
|
||||
common_tags = {
|
||||
"project" = var.project_name
|
||||
"environment" = var.environment
|
||||
"managed_by" = "terraform"
|
||||
"cloud_provider" = "oci"
|
||||
"repository" = "Claude-Code-Agent-Monitor"
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# VCN (Virtual Cloud Network)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "oci_core_vcn" "main" {
|
||||
compartment_id = var.compartment_id
|
||||
cidr_blocks = [var.vpc_cidr]
|
||||
display_name = "${local.name_prefix}-vcn"
|
||||
dns_label = replace(substr(local.name_prefix, 0, 15), "-", "")
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
# Internet Gateway
|
||||
resource "oci_core_internet_gateway" "main" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
display_name = "${local.name_prefix}-igw"
|
||||
enabled = true
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
# NAT Gateway
|
||||
resource "oci_core_nat_gateway" "main" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
display_name = "${local.name_prefix}-nat"
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
# Service Gateway
|
||||
resource "oci_core_service_gateway" "main" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
display_name = "${local.name_prefix}-sgw"
|
||||
|
||||
services {
|
||||
service_id = data.oci_core_services.all.services[0].id
|
||||
}
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
data "oci_core_services" "all" {
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["All .* Services In Oracle Services Network"]
|
||||
regex = true
|
||||
}
|
||||
}
|
||||
|
||||
# ── Route tables ────────────────────────────────────────────────────────────
|
||||
|
||||
resource "oci_core_route_table" "public" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
display_name = "${local.name_prefix}-public-rt"
|
||||
|
||||
route_rules {
|
||||
network_entity_id = oci_core_internet_gateway.main.id
|
||||
destination = "0.0.0.0/0"
|
||||
destination_type = "CIDR_BLOCK"
|
||||
}
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "oci_core_route_table" "private" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
display_name = "${local.name_prefix}-private-rt"
|
||||
|
||||
route_rules {
|
||||
network_entity_id = oci_core_nat_gateway.main.id
|
||||
destination = "0.0.0.0/0"
|
||||
destination_type = "CIDR_BLOCK"
|
||||
}
|
||||
|
||||
route_rules {
|
||||
network_entity_id = oci_core_service_gateway.main.id
|
||||
destination = data.oci_core_services.all.services[0].cidr_block
|
||||
destination_type = "SERVICE_CIDR_BLOCK"
|
||||
}
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
# ── Security lists ──────────────────────────────────────────────────────────
|
||||
|
||||
resource "oci_core_security_list" "public" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
display_name = "${local.name_prefix}-public-sl"
|
||||
|
||||
ingress_security_rules {
|
||||
protocol = "6" # TCP
|
||||
source = "0.0.0.0/0"
|
||||
source_type = "CIDR_BLOCK"
|
||||
tcp_options {
|
||||
min = 443
|
||||
max = 443
|
||||
}
|
||||
}
|
||||
|
||||
ingress_security_rules {
|
||||
protocol = "6"
|
||||
source = "0.0.0.0/0"
|
||||
source_type = "CIDR_BLOCK"
|
||||
tcp_options {
|
||||
min = 80
|
||||
max = 80
|
||||
}
|
||||
}
|
||||
|
||||
egress_security_rules {
|
||||
protocol = "all"
|
||||
destination = "0.0.0.0/0"
|
||||
destination_type = "CIDR_BLOCK"
|
||||
}
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "oci_core_security_list" "private" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
display_name = "${local.name_prefix}-private-sl"
|
||||
|
||||
ingress_security_rules {
|
||||
protocol = "6"
|
||||
source = var.vpc_cidr
|
||||
source_type = "CIDR_BLOCK"
|
||||
tcp_options {
|
||||
min = var.app_port
|
||||
max = var.app_port
|
||||
}
|
||||
}
|
||||
|
||||
ingress_security_rules {
|
||||
protocol = "6"
|
||||
source = var.vpc_cidr
|
||||
source_type = "CIDR_BLOCK"
|
||||
tcp_options {
|
||||
min = var.mcp_port
|
||||
max = var.mcp_port
|
||||
}
|
||||
}
|
||||
|
||||
# NFS (FSS)
|
||||
ingress_security_rules {
|
||||
protocol = "6"
|
||||
source = var.vpc_cidr
|
||||
source_type = "CIDR_BLOCK"
|
||||
tcp_options {
|
||||
min = 2048
|
||||
max = 2050
|
||||
}
|
||||
}
|
||||
|
||||
ingress_security_rules {
|
||||
protocol = "6"
|
||||
source = var.vpc_cidr
|
||||
source_type = "CIDR_BLOCK"
|
||||
tcp_options {
|
||||
min = 111
|
||||
max = 111
|
||||
}
|
||||
}
|
||||
|
||||
ingress_security_rules {
|
||||
protocol = "17" # UDP
|
||||
source = var.vpc_cidr
|
||||
source_type = "CIDR_BLOCK"
|
||||
udp_options {
|
||||
min = 111
|
||||
max = 111
|
||||
}
|
||||
}
|
||||
|
||||
egress_security_rules {
|
||||
protocol = "all"
|
||||
destination = "0.0.0.0/0"
|
||||
destination_type = "CIDR_BLOCK"
|
||||
}
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
# ── Subnets ─────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "oci_core_subnet" "public" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
cidr_block = var.public_subnet_cidrs[0]
|
||||
display_name = "${local.name_prefix}-public"
|
||||
dns_label = "pub"
|
||||
route_table_id = oci_core_route_table.public.id
|
||||
security_list_ids = [oci_core_security_list.public.id]
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "oci_core_subnet" "private" {
|
||||
compartment_id = var.compartment_id
|
||||
vcn_id = oci_core_vcn.main.id
|
||||
cidr_block = var.private_subnet_cidrs[0]
|
||||
display_name = "${local.name_prefix}-private"
|
||||
dns_label = "priv"
|
||||
route_table_id = oci_core_route_table.private.id
|
||||
security_list_ids = [oci_core_security_list.private.id]
|
||||
prohibit_public_ip_on_vnic = true
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# File Storage Service (FSS) – NFS for SQLite
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "oci_file_storage_file_system" "main" {
|
||||
compartment_id = var.compartment_id
|
||||
availability_domain = local.ad_name
|
||||
display_name = "${local.name_prefix}-data"
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "oci_file_storage_mount_target" "main" {
|
||||
compartment_id = var.compartment_id
|
||||
availability_domain = local.ad_name
|
||||
subnet_id = oci_core_subnet.private.id
|
||||
display_name = "${local.name_prefix}-mt"
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "oci_file_storage_export_set" "main" {
|
||||
mount_target_id = oci_file_storage_mount_target.main.id
|
||||
display_name = "${local.name_prefix}-exports"
|
||||
max_fs_stat_bytes = var.storage_size_gb * 1073741824 # GiB → bytes
|
||||
}
|
||||
|
||||
resource "oci_file_storage_export" "main" {
|
||||
export_set_id = oci_file_storage_export_set.main.id
|
||||
file_system_id = oci_file_storage_file_system.main.id
|
||||
path = "/appdata"
|
||||
|
||||
export_options {
|
||||
source = var.private_subnet_cidrs[0]
|
||||
access = "READ_WRITE"
|
||||
identity_squash = "NONE"
|
||||
require_privileged_source_port = false
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Container Instances (Blue / Green)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "oci_container_instances_container_instance" "blue" {
|
||||
compartment_id = var.compartment_id
|
||||
availability_domain = local.ad_name
|
||||
display_name = "${local.name_prefix}-blue"
|
||||
|
||||
shape = "CI.Standard.E4.Flex"
|
||||
shape_config {
|
||||
ocpus = var.cpu / 1000.0
|
||||
memory_in_gbs = var.memory / 1024.0
|
||||
}
|
||||
|
||||
vnics {
|
||||
subnet_id = oci_core_subnet.private.id
|
||||
is_public_ip_assigned = false
|
||||
}
|
||||
|
||||
containers {
|
||||
display_name = "app"
|
||||
image_url = var.app_container_image
|
||||
|
||||
environment_variables = var.environment_variables
|
||||
|
||||
health_checks {
|
||||
health_check_type = "HTTP"
|
||||
port = var.app_port
|
||||
path = var.health_check_path
|
||||
interval_in_seconds = 30
|
||||
timeout_in_seconds = 5
|
||||
}
|
||||
|
||||
resource_config {
|
||||
vcpus_limit = var.cpu / 1000.0
|
||||
memory_limit_in_gbs = var.memory / 1024.0
|
||||
}
|
||||
|
||||
volume_mounts {
|
||||
mount_path = "/app/data"
|
||||
volume_name = "app-data"
|
||||
is_read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "containers" {
|
||||
for_each = var.mcp_container_image != "" ? [1] : []
|
||||
content {
|
||||
display_name = "mcp-sidecar"
|
||||
image_url = var.mcp_container_image
|
||||
|
||||
environment_variables = {
|
||||
NODE_ENV = "production"
|
||||
MCP_PORT = tostring(var.mcp_port)
|
||||
}
|
||||
|
||||
resource_config {
|
||||
vcpus_limit = 0.25
|
||||
memory_limit_in_gbs = 0.25
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# NOTE: OCI Container Instances only support EMPTYDIR and CONFIGFILE volume
|
||||
# types. For persistent NFS (FSS) storage, mount via the container entrypoint
|
||||
# using the mount target IP from oci_file_storage_mount_target.main, or
|
||||
# migrate to OCI Kubernetes Engine (OKE) which supports NFS PersistentVolumes.
|
||||
volumes {
|
||||
name = "app-data"
|
||||
volume_type = "EMPTYDIR"
|
||||
backing_store = "EPHEMERAL_STORAGE"
|
||||
}
|
||||
|
||||
freeform_tags = merge(local.common_tags, {
|
||||
deployment_slot = "blue"
|
||||
})
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
freeform_tags["last_deployed"],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "oci_container_instances_container_instance" "green" {
|
||||
count = var.active_deployment_slot == "green" || var.green_weight > 0 ? 1 : 0
|
||||
|
||||
compartment_id = var.compartment_id
|
||||
availability_domain = local.ad_name
|
||||
display_name = "${local.name_prefix}-green"
|
||||
|
||||
shape = "CI.Standard.E4.Flex"
|
||||
shape_config {
|
||||
ocpus = var.cpu / 1000.0
|
||||
memory_in_gbs = var.memory / 1024.0
|
||||
}
|
||||
|
||||
vnics {
|
||||
subnet_id = oci_core_subnet.private.id
|
||||
is_public_ip_assigned = false
|
||||
}
|
||||
|
||||
containers {
|
||||
display_name = "app"
|
||||
image_url = var.app_container_image
|
||||
|
||||
environment_variables = var.environment_variables
|
||||
|
||||
health_checks {
|
||||
health_check_type = "HTTP"
|
||||
port = var.app_port
|
||||
path = var.health_check_path
|
||||
interval_in_seconds = 30
|
||||
timeout_in_seconds = 5
|
||||
}
|
||||
|
||||
resource_config {
|
||||
vcpus_limit = var.cpu / 1000.0
|
||||
memory_limit_in_gbs = var.memory / 1024.0
|
||||
}
|
||||
|
||||
volume_mounts {
|
||||
mount_path = "/app/data"
|
||||
volume_name = "app-data"
|
||||
is_read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
# NOTE: OCI Container Instances only support EMPTYDIR and CONFIGFILE volume
|
||||
# types. See blue instance comment for FSS mounting guidance.
|
||||
volumes {
|
||||
name = "app-data"
|
||||
volume_type = "EMPTYDIR"
|
||||
backing_store = "EPHEMERAL_STORAGE"
|
||||
}
|
||||
|
||||
freeform_tags = merge(local.common_tags, {
|
||||
deployment_slot = "green"
|
||||
})
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Flexible Load Balancer
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "oci_load_balancer_load_balancer" "main" {
|
||||
compartment_id = var.compartment_id
|
||||
display_name = "${local.name_prefix}-lb"
|
||||
shape = "flexible"
|
||||
|
||||
shape_details {
|
||||
minimum_bandwidth_in_mbps = var.environment == "production" ? 100 : 10
|
||||
maximum_bandwidth_in_mbps = var.environment == "production" ? 1000 : 100
|
||||
}
|
||||
|
||||
subnet_ids = [oci_core_subnet.public.id]
|
||||
|
||||
is_private = false
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
# Backend set with health check
|
||||
resource "oci_load_balancer_backend_set" "app" {
|
||||
load_balancer_id = oci_load_balancer_load_balancer.main.id
|
||||
name = "${local.name_prefix}-app-bs"
|
||||
policy = "ROUND_ROBIN"
|
||||
|
||||
session_persistence_configuration {
|
||||
cookie_name = "CCAM_SESSION"
|
||||
is_secure = true
|
||||
}
|
||||
|
||||
health_checker {
|
||||
protocol = "HTTP"
|
||||
port = var.app_port
|
||||
url_path = var.health_check_path
|
||||
return_code = 200
|
||||
interval_ms = var.health_check_interval * 1000
|
||||
timeout_in_millis = var.health_check_timeout * 1000
|
||||
retries = var.health_check_unhealthy_threshold
|
||||
}
|
||||
}
|
||||
|
||||
# Blue backend
|
||||
resource "oci_load_balancer_backend" "blue" {
|
||||
load_balancer_id = oci_load_balancer_load_balancer.main.id
|
||||
backendset_name = oci_load_balancer_backend_set.app.name
|
||||
ip_address = oci_container_instances_container_instance.blue.vnics[0].private_ip
|
||||
port = var.app_port
|
||||
weight = var.blue_weight
|
||||
}
|
||||
|
||||
# Green backend
|
||||
resource "oci_load_balancer_backend" "green" {
|
||||
count = length(oci_container_instances_container_instance.green) > 0 ? 1 : 0
|
||||
|
||||
load_balancer_id = oci_load_balancer_load_balancer.main.id
|
||||
backendset_name = oci_load_balancer_backend_set.app.name
|
||||
ip_address = oci_container_instances_container_instance.green[0].vnics[0].private_ip
|
||||
port = var.app_port
|
||||
weight = var.green_weight
|
||||
}
|
||||
|
||||
# HTTP listener
|
||||
resource "oci_load_balancer_listener" "http" {
|
||||
load_balancer_id = oci_load_balancer_load_balancer.main.id
|
||||
name = "${local.name_prefix}-http"
|
||||
default_backend_set_name = oci_load_balancer_backend_set.app.name
|
||||
port = 80
|
||||
protocol = "HTTP"
|
||||
|
||||
connection_configuration {
|
||||
idle_timeout_in_seconds = 300 # WebSocket support
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# OCI Monitoring – Alarms and Notifications
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "oci_ons_notification_topic" "alerts" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
compartment_id = var.compartment_id
|
||||
name = "${local.name_prefix}-alerts"
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "oci_ons_subscription" "email" {
|
||||
count = var.enable_monitoring && var.alert_email != "" ? 1 : 0
|
||||
|
||||
compartment_id = var.compartment_id
|
||||
topic_id = oci_ons_notification_topic.alerts[0].id
|
||||
protocol = "EMAIL"
|
||||
endpoint = var.alert_email
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "oci_monitoring_alarm" "lb_unhealthy" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
compartment_id = var.compartment_id
|
||||
display_name = "${local.name_prefix}-unhealthy-backends"
|
||||
namespace = "oci_lbaas"
|
||||
query = "UnHealthyBackendCount[1m]{resourceId = \"${oci_load_balancer_load_balancer.main.id}\"}.max() > 0"
|
||||
severity = "CRITICAL"
|
||||
is_enabled = true
|
||||
pending_duration = "PT5M"
|
||||
|
||||
destinations = var.alert_email != "" ? [oci_ons_notification_topic.alerts[0].id] : []
|
||||
|
||||
message_format = "ONS_OPTIMIZED"
|
||||
body = "Unhealthy backends detected for ${local.name_prefix} load balancer"
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "oci_monitoring_alarm" "lb_5xx" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
compartment_id = var.compartment_id
|
||||
display_name = "${local.name_prefix}-high-5xx"
|
||||
namespace = "oci_lbaas"
|
||||
query = "HttpResponses5xx[1m]{resourceId = \"${oci_load_balancer_load_balancer.main.id}\"}.sum() > 10"
|
||||
severity = "WARNING"
|
||||
is_enabled = true
|
||||
pending_duration = "PT5M"
|
||||
|
||||
destinations = var.alert_email != "" ? [oci_ons_notification_topic.alerts[0].id] : []
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "oci_monitoring_alarm" "high_latency" {
|
||||
count = var.enable_monitoring ? 1 : 0
|
||||
|
||||
compartment_id = var.compartment_id
|
||||
display_name = "${local.name_prefix}-high-latency"
|
||||
namespace = "oci_lbaas"
|
||||
query = "BackendTimeFirstByte[1m]{resourceId = \"${oci_load_balancer_load_balancer.main.id}\"}.percentile(0.99) > 2000"
|
||||
severity = "WARNING"
|
||||
is_enabled = true
|
||||
pending_duration = "PT5M"
|
||||
|
||||
destinations = var.alert_email != "" ? [oci_ons_notification_topic.alerts[0].id] : []
|
||||
|
||||
freeform_tags = local.common_tags
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# OCI provider outputs
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
output "application_url" {
|
||||
description = "Public URL of the Claude Code Agent Monitor dashboard"
|
||||
value = "http://${oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address}"
|
||||
}
|
||||
|
||||
output "load_balancer_ip" {
|
||||
description = "Public IP address of the load balancer"
|
||||
value = oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address
|
||||
}
|
||||
|
||||
output "vcn_id" {
|
||||
description = "OCID of the VCN"
|
||||
value = oci_core_vcn.main.id
|
||||
}
|
||||
|
||||
output "blue_instance_id" {
|
||||
description = "OCID of the blue container instance"
|
||||
value = oci_container_instances_container_instance.blue.id
|
||||
}
|
||||
|
||||
output "green_instance_id" {
|
||||
description = "OCID of the green container instance (if deployed)"
|
||||
value = length(oci_container_instances_container_instance.green) > 0 ? oci_container_instances_container_instance.green[0].id : ""
|
||||
}
|
||||
|
||||
output "file_system_id" {
|
||||
description = "OCID of the File Storage file system"
|
||||
value = oci_file_storage_file_system.main.id
|
||||
}
|
||||
|
||||
output "mount_target_ip" {
|
||||
description = "IP address of the FSS mount target"
|
||||
value = oci_file_storage_mount_target.main.ip_address
|
||||
}
|
||||
|
||||
output "load_balancer_id" {
|
||||
description = "OCID of the load balancer"
|
||||
value = oci_load_balancer_load_balancer.main.id
|
||||
}
|
||||
|
||||
output "compartment_id" {
|
||||
description = "OCI compartment OCID"
|
||||
value = var.compartment_id
|
||||
}
|
||||
|
||||
output "region" {
|
||||
description = "OCI region"
|
||||
value = var.region
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# OCI provider – Terraform and provider constraints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
oci = {
|
||||
source = "oracle/oci"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# OCI provider variables
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project identifier used in resource naming"
|
||||
type = string
|
||||
default = "claude-agent-monitor"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment: dev, staging, or production"
|
||||
type = string
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "production"], var.environment)
|
||||
error_message = "environment must be one of: dev, staging, production."
|
||||
}
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "OCI region for resource deployment"
|
||||
type = string
|
||||
default = "us-ashburn-1"
|
||||
}
|
||||
|
||||
variable "tenancy_id" {
|
||||
description = "OCI tenancy OCID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "compartment_id" {
|
||||
description = "OCI compartment OCID for resource deployment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Additional freeform tags to apply to all resources"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "vpc_cidr" {
|
||||
description = "CIDR block for the VCN"
|
||||
type = string
|
||||
default = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
variable "public_subnet_cidrs" {
|
||||
description = "CIDR blocks for public subnets"
|
||||
type = list(string)
|
||||
default = ["10.0.1.0/24"]
|
||||
}
|
||||
|
||||
variable "private_subnet_cidrs" {
|
||||
description = "CIDR blocks for private subnets"
|
||||
type = list(string)
|
||||
default = ["10.0.11.0/24"]
|
||||
}
|
||||
|
||||
# ── Compute ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_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 = "Application container port"
|
||||
type = number
|
||||
default = 4820
|
||||
}
|
||||
|
||||
variable "mcp_port" {
|
||||
description = "MCP sidecar container port"
|
||||
type = number
|
||||
default = 8819
|
||||
}
|
||||
|
||||
variable "cpu" {
|
||||
description = "CPU millicores (converted to OCPUs: 1000m = 1 OCPU)"
|
||||
type = number
|
||||
default = 512
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory in MiB (converted to GiB for OCI)"
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "environment_variables" {
|
||||
description = "Environment variables for the application container"
|
||||
type = map(string)
|
||||
default = {
|
||||
NODE_ENV = "production"
|
||||
DASHBOARD_PORT = "4820"
|
||||
}
|
||||
}
|
||||
|
||||
# ── Deployment ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "active_deployment_slot" {
|
||||
description = "Active deployment slot: blue or green"
|
||||
type = string
|
||||
default = "blue"
|
||||
validation {
|
||||
condition = contains(["blue", "green"], var.active_deployment_slot)
|
||||
error_message = "active_deployment_slot must be blue or green."
|
||||
}
|
||||
}
|
||||
|
||||
variable "blue_weight" {
|
||||
description = "Traffic weight for blue backend (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 backend (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."
|
||||
}
|
||||
}
|
||||
|
||||
# ── Storage ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "storage_size_gb" {
|
||||
description = "FSS export size limit in GiB"
|
||||
type = number
|
||||
default = 50
|
||||
}
|
||||
|
||||
# ── Health check ────────────────────────────────────────────────────────────
|
||||
|
||||
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_unhealthy_threshold" {
|
||||
description = "Consecutive failures to mark unhealthy"
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
# ── Monitoring ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "enable_monitoring" {
|
||||
description = "Enable OCI Monitoring alarms and notifications"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "alert_email" {
|
||||
description = "Email address for alarm notifications"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Root module variables – Claude Code Agent Monitor
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Provider selection ──────────────────────────────────────────────────────
|
||||
|
||||
variable "cloud_provider" {
|
||||
description = "Target cloud provider: aws, gcp, azure, or oci"
|
||||
type = string
|
||||
validation {
|
||||
condition = contains(["aws", "gcp", "azure", "oci"], var.cloud_provider)
|
||||
error_message = "cloud_provider must be one of: aws, gcp, azure, oci."
|
||||
}
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "Cloud provider region for resource deployment"
|
||||
type = string
|
||||
}
|
||||
|
||||
# ── Project metadata ────────────────────────────────────────────────────────
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project identifier used in resource naming and tagging"
|
||||
type = string
|
||||
default = "claude-agent-monitor"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment: dev, staging, or production"
|
||||
type = string
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "production"], var.environment)
|
||||
error_message = "environment must be one of: dev, staging, production."
|
||||
}
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Additional tags to apply to all resources"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
# ── Networking ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "vpc_cidr" {
|
||||
description = "CIDR block for the VPC / VNet / VCN"
|
||||
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"]
|
||||
}
|
||||
|
||||
# ── Compute ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_container_image" {
|
||||
description = "Container image URI for the main application"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "mcp_container_image" {
|
||||
description = "Container image URI for the MCP sidecar"
|
||||
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 each container instance (e.g. 256, 512, 1024)"
|
||||
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 (valid Fargate CPU values)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory in MiB for each container instance"
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "min_replicas" {
|
||||
description = "Minimum number of container replicas"
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "max_replicas" {
|
||||
description = "Maximum number of container replicas for auto-scaling"
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
variable "desired_replicas" {
|
||||
description = "Desired number of container replicas at steady state"
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "environment_variables" {
|
||||
description = "Environment variables passed to the application container"
|
||||
type = map(string)
|
||||
default = {
|
||||
NODE_ENV = "production"
|
||||
DASHBOARD_PORT = "4820"
|
||||
}
|
||||
}
|
||||
|
||||
# ── Deployment strategy ─────────────────────────────────────────────────────
|
||||
|
||||
variable "deployment_strategy" {
|
||||
description = "Deployment strategy: rolling, blue-green, or canary"
|
||||
type = string
|
||||
default = "rolling"
|
||||
validation {
|
||||
condition = contains(["rolling", "blue-green", "canary"], var.deployment_strategy)
|
||||
error_message = "deployment_strategy must be one of: rolling, blue-green, canary."
|
||||
}
|
||||
}
|
||||
|
||||
variable "active_deployment_slot" {
|
||||
description = "Active deployment slot for blue-green: blue or green"
|
||||
type = string
|
||||
default = "blue"
|
||||
validation {
|
||||
condition = contains(["blue", "green"], var.active_deployment_slot)
|
||||
error_message = "active_deployment_slot must be blue or green."
|
||||
}
|
||||
}
|
||||
|
||||
variable "blue_weight" {
|
||||
description = "Traffic weight percentage for the blue deployment slot (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 percentage for the green deployment slot (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."
|
||||
}
|
||||
}
|
||||
|
||||
# ── TLS / Domain ────────────────────────────────────────────────────────────
|
||||
|
||||
variable "domain_name" {
|
||||
description = "Fully qualified domain name for the application"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "tls_certificate_arn" {
|
||||
description = "ARN / ID of the TLS certificate for HTTPS termination"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ── Monitoring ──────────────────────────────────────────────────────────────
|
||||
|
||||
variable "enable_monitoring" {
|
||||
description = "Enable monitoring, alerting, and log aggregation"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "alert_email" {
|
||||
description = "Email address for monitoring alert notifications"
|
||||
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 (1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, or 3653)."
|
||||
}
|
||||
}
|
||||
|
||||
# ── Storage ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "storage_size_gb" {
|
||||
description = "Persistent storage size in GiB for the SQLite database"
|
||||
type = number
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "enable_storage_backup" {
|
||||
description = "Enable automated backup of persistent storage"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
# ── Auto-scaling ────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# ── Health check ────────────────────────────────────────────────────────────
|
||||
|
||||
variable "health_check_path" {
|
||||
description = "HTTP path for application health checks"
|
||||
type = string
|
||||
default = "/api/health"
|
||||
}
|
||||
|
||||
variable "health_check_interval" {
|
||||
description = "Interval in seconds between health checks"
|
||||
type = number
|
||||
default = 30
|
||||
}
|
||||
|
||||
variable "health_check_timeout" {
|
||||
description = "Timeout in seconds for each health check request"
|
||||
type = number
|
||||
default = 5
|
||||
}
|
||||
|
||||
variable "health_check_healthy_threshold" {
|
||||
description = "Consecutive successes required to mark target healthy"
|
||||
type = number
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "health_check_unhealthy_threshold" {
|
||||
description = "Consecutive failures required to mark target unhealthy"
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Claude Code Agent Monitor – Terraform version constraints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
azurerm = {
|
||||
source = "hashicorp/azurerm"
|
||||
version = "~> 3.80"
|
||||
}
|
||||
oci = {
|
||||
source = "oracle/oci"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user