feat(phase-2): workstream 8 — Multi-Region Terraform Deployment

AWS environment:
- VPC (3-AZ, public + private subnets, NAT gateways, VPC endpoints for ECR/SM/CW)
- ECS Fargate service (sentryagent/agentidp) — secrets from Secrets Manager
- RDS PostgreSQL 14 (Multi-AZ, encrypted, VPC-internal, storage autoscaling)
- ElastiCache Redis 7 (primary + replica, at-rest + in-transit encryption)
- ALB with HTTPS/443, HTTP→HTTPS redirect, ACM certificate
- Route 53 alias record

GCP environment:
- VPC + private services access + Serverless VPC connector
- Cloud Run service — secrets from Secret Manager
- Cloud SQL PostgreSQL 14 (private IP, no public endpoint)
- Cloud Memorystore Redis 7 (VPC-internal, AUTH enabled)

Shared:
- 4 reusable modules: agentidp (dual AWS/GCP), rds, redis, lb
- No hardcoded secrets; all sensitive vars marked sensitive=true
- terraform.tfvars.example for both environments
- docs/devops/deployment.md — AWS + GCP step-by-step walkthrough, rollback procedures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-03-29 06:25:14 +00:00
parent a504964e5f
commit 6913d62648
22 changed files with 4138 additions and 8 deletions

View File

@@ -0,0 +1,477 @@
################################################################################
# Environment: gcp
# Main — SentryAgent.ai AgentIdP on Google Cloud Platform
#
# Architecture:
# Internet → Cloud Run (Google-managed TLS, auto-scaling) →
# Cloud SQL PostgreSQL 14 (private IP, REGIONAL HA) +
# Memorystore Redis 7 (STANDARD_HA, in-transit encryption)
# via Serverless VPC Access connector
#
# All secrets stored in GCP Secret Manager — Cloud Run reads them at startup.
# No sensitive values in state (except where Terraform internals require it).
################################################################################
terraform {
required_version = ">= 1.6.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 5.20.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 5.20.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.6.0"
}
}
# Remote state — configure your backend here.
# Example using GCS:
#
# backend "gcs" {
# bucket = "sentryagent-terraform-state"
# prefix = "agentidp/gcp/production"
# }
}
provider "google" {
project = var.project_id
region = var.region
}
provider "google-beta" {
project = var.project_id
region = var.region
}
################################################################################
# Enable required GCP APIs
################################################################################
resource "google_project_service" "apis" {
for_each = toset([
"run.googleapis.com",
"sqladmin.googleapis.com",
"redis.googleapis.com",
"vpcaccess.googleapis.com",
"secretmanager.googleapis.com",
"servicenetworking.googleapis.com",
"cloudresourcemanager.googleapis.com",
"iam.googleapis.com",
])
project = var.project_id
service = each.value
disable_on_destroy = false
}
################################################################################
# Locals
################################################################################
locals {
name_prefix = "${var.project}-${var.environment}"
common_labels = {
environment = var.environment
project = replace(var.project, "-", "_")
managed_by = "terraform"
}
}
################################################################################
# VPC Network
################################################################################
resource "google_compute_network" "main" {
name = "${local.name_prefix}-vpc"
auto_create_subnetworks = false
project = var.project_id
depends_on = [google_project_service.apis]
}
resource "google_compute_subnetwork" "private" {
name = "${local.name_prefix}-private-subnet"
ip_cidr_range = var.vpc_cidr
region = var.region
network = google_compute_network.main.id
project = var.project_id
private_ip_google_access = true
log_config {
aggregation_interval = "INTERVAL_10_MIN"
flow_sampling = 0.5
metadata = "INCLUDE_ALL_METADATA"
}
}
################################################################################
# Private Services Access — required for Cloud SQL private IP
################################################################################
resource "google_compute_global_address" "private_services" {
name = "${local.name_prefix}-private-services-range"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 20
network = google_compute_network.main.id
project = var.project_id
}
resource "google_service_networking_connection" "private_services" {
network = google_compute_network.main.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.private_services.name]
depends_on = [google_project_service.apis]
}
################################################################################
# Serverless VPC Access Connector
# Cloud Run uses this to reach Cloud SQL (private IP) and Memorystore.
################################################################################
resource "google_vpc_access_connector" "main" {
name = "${local.name_prefix}-connector"
region = var.region
project = var.project_id
ip_cidr_range = var.vpc_connector_cidr
network = google_compute_network.main.name
min_instances = 2
max_instances = 10
machine_type = "e2-micro"
depends_on = [google_project_service.apis]
}
################################################################################
# Service Account for Cloud Run
################################################################################
resource "google_service_account" "cloud_run" {
account_id = "${var.project}-${var.environment}-run-sa"
display_name = "AgentIdP Cloud Run Service Account (${var.environment})"
project = var.project_id
}
################################################################################
# Secret Manager — create secrets and grant the SA access
################################################################################
resource "google_secret_manager_secret" "database_url" {
secret_id = "${local.name_prefix}-database-url"
project = var.project_id
replication {
auto {}
}
labels = local.common_labels
depends_on = [google_project_service.apis]
}
resource "google_secret_manager_secret_version" "database_url" {
secret = google_secret_manager_secret.database_url.id
# Build the DATABASE_URL from Cloud SQL private IP output.
secret_data = "postgresql://${var.db_username}:${var.db_password}@${google_sql_database_instance.main.private_ip_address}:5432/${var.db_name}?sslmode=require"
depends_on = [google_sql_database_instance.main]
}
resource "google_secret_manager_secret" "redis_url" {
secret_id = "${local.name_prefix}-redis-url"
project = var.project_id
replication {
auto {}
}
labels = local.common_labels
depends_on = [google_project_service.apis]
}
resource "google_secret_manager_secret_version" "redis_url" {
secret = google_secret_manager_secret.redis_url.id
# Memorystore Redis with in-transit encryption uses the rediss:// scheme.
secret_data = "rediss://${google_redis_instance.main.host}:${google_redis_instance.main.port}"
depends_on = [google_redis_instance.main]
}
resource "google_secret_manager_secret" "jwt_private_key" {
secret_id = "${local.name_prefix}-jwt-private-key"
project = var.project_id
replication {
auto {}
}
labels = local.common_labels
depends_on = [google_project_service.apis]
}
resource "google_secret_manager_secret_version" "jwt_private_key" {
secret = google_secret_manager_secret.jwt_private_key.id
secret_data = var.jwt_private_key
}
resource "google_secret_manager_secret" "jwt_public_key" {
secret_id = "${local.name_prefix}-jwt-public-key"
project = var.project_id
replication {
auto {}
}
labels = local.common_labels
depends_on = [google_project_service.apis]
}
resource "google_secret_manager_secret_version" "jwt_public_key" {
secret = google_secret_manager_secret.jwt_public_key.id
secret_data = var.jwt_public_key
}
resource "google_secret_manager_secret" "vault_token" {
count = var.vault_token != "" ? 1 : 0
secret_id = "${local.name_prefix}-vault-token"
project = var.project_id
replication {
auto {}
}
labels = local.common_labels
depends_on = [google_project_service.apis]
}
resource "google_secret_manager_secret_version" "vault_token" {
count = var.vault_token != "" ? 1 : 0
secret = google_secret_manager_secret.vault_token[0].id
secret_data = var.vault_token
}
# Grant the Cloud Run SA access to each secret
resource "google_secret_manager_secret_iam_member" "run_database_url" {
project = var.project_id
secret_id = google_secret_manager_secret.database_url.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloud_run.email}"
}
resource "google_secret_manager_secret_iam_member" "run_redis_url" {
project = var.project_id
secret_id = google_secret_manager_secret.redis_url.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloud_run.email}"
}
resource "google_secret_manager_secret_iam_member" "run_jwt_private_key" {
project = var.project_id
secret_id = google_secret_manager_secret.jwt_private_key.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloud_run.email}"
}
resource "google_secret_manager_secret_iam_member" "run_jwt_public_key" {
project = var.project_id
secret_id = google_secret_manager_secret.jwt_public_key.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloud_run.email}"
}
resource "google_secret_manager_secret_iam_member" "run_vault_token" {
count = var.vault_token != "" ? 1 : 0
project = var.project_id
secret_id = google_secret_manager_secret.vault_token[0].secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloud_run.email}"
}
################################################################################
# Cloud SQL — PostgreSQL 14, private IP, REGIONAL HA
################################################################################
resource "google_sql_database_instance" "main" {
name = "${local.name_prefix}-pg14"
database_version = "POSTGRES_14"
region = var.region
project = var.project_id
deletion_protection = var.deletion_protection
settings {
tier = var.db_tier
availability_type = var.db_availability_type
disk_type = "PD_SSD"
disk_size = 50
disk_autoresize = true
ip_configuration {
ipv4_enabled = false # No public IP
private_network = google_compute_network.main.id
require_ssl = true
}
backup_configuration {
enabled = true
start_time = "03:00"
point_in_time_recovery_enabled = true
transaction_log_retention_days = 7
backup_retention_settings {
retained_backups = 7
retention_unit = "COUNT"
}
}
maintenance_window {
day = 7 # Sunday
hour = 5
update_track = "stable"
}
insights_config {
query_insights_enabled = true
query_string_length = 1024
record_application_tags = true
record_client_address = false
}
database_flags {
name = "log_connections"
value = "on"
}
database_flags {
name = "log_disconnections"
value = "on"
}
database_flags {
name = "log_min_duration_statement"
value = "1000"
}
user_labels = local.common_labels
}
depends_on = [google_service_networking_connection.private_services]
}
resource "google_sql_database" "main" {
name = var.db_name
instance = google_sql_database_instance.main.name
project = var.project_id
}
resource "google_sql_user" "app" {
name = var.db_username
instance = google_sql_database_instance.main.name
password = var.db_password
project = var.project_id
}
################################################################################
# Memorystore Redis 7 — STANDARD_HA (primary + replica), TLS enabled
################################################################################
resource "google_redis_instance" "main" {
name = "${local.name_prefix}-redis"
tier = var.memorystore_tier
memory_size_gb = var.memorystore_memory_size_gb
region = var.region
project = var.project_id
redis_version = var.memorystore_redis_version
# Private connectivity via the VPC
authorized_network = google_compute_network.main.id
connect_mode = "PRIVATE_SERVICE_ACCESS"
# TLS in transit
transit_encryption_mode = "SERVER_AUTHENTICATION"
# No AUTH token for Memorystore — access is controlled by VPC network policy.
# If AUTH is required, set auth_enabled = true and read the generated auth_string output.
auth_enabled = true
redis_configs = {
lazyfree-lazy-eviction = "yes"
lazyfree-lazy-expire = "yes"
}
maintenance_policy {
weekly_maintenance_window {
day = "SUNDAY"
start_time {
hours = 6
minutes = 0
seconds = 0
nanos = 0
}
}
}
labels = local.common_labels
depends_on = [google_service_networking_connection.private_services]
}
################################################################################
# Module: AgentIdP (Cloud Run)
################################################################################
module "agentidp" {
source = "../../modules/agentidp"
provider_type = "gcp"
environment = var.environment
project = var.project
app_image = "sentryagent/agentidp:${var.app_image_tag}"
app_port = 3000
gcp_project_id = var.project_id
gcp_region = var.region
gcp_service_account_email = google_service_account.cloud_run.email
gcp_vpc_connector_name = google_vpc_access_connector.main.id
gcp_min_instances = var.cloud_run_min_instances
gcp_max_instances = var.cloud_run_max_instances
gcp_cpu = var.cloud_run_cpu
gcp_memory = var.cloud_run_memory
gcp_cors_origin = var.cors_origin
gcp_policy_dir = "/app/policies"
gcp_vault_addr = var.vault_addr
gcp_vault_mount = var.vault_mount
gcp_secret_database_url_id = google_secret_manager_secret.database_url.secret_id
gcp_secret_redis_url_id = google_secret_manager_secret.redis_url.secret_id
gcp_secret_jwt_private_key_id = google_secret_manager_secret.jwt_private_key.secret_id
gcp_secret_jwt_public_key_id = google_secret_manager_secret.jwt_public_key.secret_id
gcp_secret_vault_token_id = var.vault_token != "" ? google_secret_manager_secret.vault_token[0].secret_id : ""
depends_on = [
google_secret_manager_secret_version.database_url,
google_secret_manager_secret_version.redis_url,
google_secret_manager_secret_version.jwt_private_key,
google_secret_manager_secret_version.jwt_public_key,
google_secret_manager_secret_iam_member.run_database_url,
google_secret_manager_secret_iam_member.run_redis_url,
google_secret_manager_secret_iam_member.run_jwt_private_key,
google_secret_manager_secret_iam_member.run_jwt_public_key,
]
}