57dc91585d
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.
248 lines
8.5 KiB
Terraform
248 lines
8.5 KiB
Terraform
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 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."
|
||
}
|
||
}
|