feat: Claude Code Monitor — lanes, pipelines and a merged workspace

Internal SmartGift build of a Claude Code monitoring dashboard.

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

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

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit 57dc91585d
783 changed files with 221743 additions and 0 deletions
+264
View File
@@ -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
}
+678
View File
@@ -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 = ""
}
+602
View File
@@ -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 = ""
}