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,
]
}

View File

@@ -0,0 +1,64 @@
################################################################################
# Environment: gcp
# Outputs
################################################################################
output "service_url" {
description = "Public HTTPS URL of the AgentIdP Cloud Run service (Google-managed TLS)."
value = module.agentidp.gcp_cloud_run_service_url
}
output "cloud_run_service_name" {
description = "Name of the Cloud Run service."
value = module.agentidp.gcp_cloud_run_service_name
}
output "cloud_run_service_id" {
description = "Full resource ID of the Cloud Run service."
value = module.agentidp.gcp_cloud_run_service_id
}
output "cloud_sql_instance_name" {
description = "Cloud SQL instance name."
value = google_sql_database_instance.main.name
}
output "cloud_sql_private_ip" {
description = "Private IP address of the Cloud SQL instance."
value = google_sql_database_instance.main.private_ip_address
}
output "cloud_sql_connection_name" {
description = "Cloud SQL instance connection name (project:region:name) for Cloud SQL Proxy."
value = google_sql_database_instance.main.connection_name
}
output "memorystore_host" {
description = "IP address of the Memorystore Redis primary endpoint."
value = google_redis_instance.main.host
}
output "memorystore_port" {
description = "Port of the Memorystore Redis instance."
value = google_redis_instance.main.port
}
output "memorystore_id" {
description = "Fully-qualified resource ID of the Memorystore Redis instance."
value = google_redis_instance.main.id
}
output "vpc_network_name" {
description = "Name of the VPC network created for this deployment."
value = google_compute_network.main.name
}
output "vpc_connector_name" {
description = "Serverless VPC Access connector name used by Cloud Run."
value = google_vpc_access_connector.main.name
}
output "cloud_run_service_account_email" {
description = "Email of the service account attached to the Cloud Run service."
value = google_service_account.cloud_run.email
}

View File

@@ -0,0 +1,70 @@
# ─────────────────────────────────────────────────────────────────────────────
# terraform/environments/gcp/terraform.tfvars.example
#
# Copy this file to terraform.tfvars and fill in real values.
# NEVER commit terraform.tfvars to version control — it contains secrets.
#
# All sensitive variables (db_password, jwt_*, vault_token) must be provided
# via this file or as TF_VAR_* environment variables in your CI/CD pipeline.
# ─────────────────────────────────────────────────────────────────────────────
# ── GCP project & region ──────────────────────────────────────────────────────
project_id = "your-gcp-project-id"
region = "us-central1"
environment = "production"
project = "sentryagent-agentidp"
# ── Application image ─────────────────────────────────────────────────────────
app_image_tag = "1.0.0"
# ── Networking ────────────────────────────────────────────────────────────────
vpc_cidr = "10.1.0.0/24"
vpc_connector_cidr = "10.8.0.0/28"
# ── Database ──────────────────────────────────────────────────────────────────
db_tier = "db-g1-small"
db_name = "sentryagent_idp"
db_username = "sentryagent"
db_availability_type = "REGIONAL"
# ── Secrets — REPLACE ALL VALUES BELOW ───────────────────────────────────────
# Password for Cloud SQL PostgreSQL user
db_password = "REPLACE_WITH_STRONG_RANDOM_PASSWORD"
# RSA-2048 key pair for JWT signing/verification.
# Generate with:
# openssl genrsa -out private.pem 2048
# openssl rsa -in private.pem -pubout -out public.pem
jwt_private_key = "-----BEGIN RSA PRIVATE KEY-----\nREPLACE_WITH_ACTUAL_PRIVATE_KEY_CONTENTS\n-----END RSA PRIVATE KEY-----"
jwt_public_key = "-----BEGIN PUBLIC KEY-----\nREPLACE_WITH_ACTUAL_PUBLIC_KEY_CONTENTS\n-----END PUBLIC KEY-----"
# HashiCorp Vault (optional — leave empty strings to disable Vault integration)
vault_addr = ""
vault_token = ""
vault_mount = "secret"
# ── Application configuration ─────────────────────────────────────────────────
cors_origin = "*"
# ── Cloud Run scaling ─────────────────────────────────────────────────────────
cloud_run_min_instances = 1
cloud_run_max_instances = 10
cloud_run_cpu = "1"
cloud_run_memory = "512Mi"
# ── Memorystore Redis ─────────────────────────────────────────────────────────
memorystore_memory_size_gb = 1
memorystore_redis_version = "REDIS_7_0"
memorystore_tier = "STANDARD_HA"
# ── Protection ────────────────────────────────────────────────────────────────
deletion_protection = true

View File

@@ -0,0 +1,175 @@
################################################################################
# Environment: gcp
# Variables
################################################################################
variable "project_id" {
description = "GCP project ID where all resources will be created."
type = string
}
variable "region" {
description = "GCP region for all resources."
type = string
default = "us-central1"
}
variable "environment" {
description = "Deployment environment (e.g. production, staging)."
type = string
default = "production"
}
variable "project" {
description = "Project identifier — used in resource names and labels."
type = string
default = "sentryagent-agentidp"
}
variable "app_image_tag" {
description = "Docker image tag to deploy (e.g. '1.2.3')."
type = string
}
################################################################################
# Networking
################################################################################
variable "vpc_cidr" {
description = "CIDR range for the VPC subnet used by Cloud Run and Cloud SQL."
type = string
default = "10.1.0.0/24"
}
variable "vpc_connector_cidr" {
description = "CIDR range for the Serverless VPC Access connector (/28 required)."
type = string
default = "10.8.0.0/28"
}
################################################################################
# Database
################################################################################
variable "db_tier" {
description = "Cloud SQL instance tier (machine type)."
type = string
default = "db-g1-small"
}
variable "db_name" {
description = "Name of the PostgreSQL database to create."
type = string
default = "sentryagent_idp"
}
variable "db_username" {
description = "PostgreSQL user for the application."
type = string
default = "sentryagent"
}
variable "db_availability_type" {
description = "Cloud SQL availability type: REGIONAL (HA) or ZONAL."
type = string
default = "REGIONAL"
}
################################################################################
# Secrets — all marked sensitive; provide via tfvars or environment variables
################################################################################
variable "db_password" {
description = "Password for the Cloud SQL PostgreSQL user. Stored in Secret Manager."
type = string
sensitive = true
}
variable "jwt_private_key" {
description = "PEM-encoded RSA-2048 private key for signing JWTs. Stored in Secret Manager."
type = string
sensitive = true
}
variable "jwt_public_key" {
description = "PEM-encoded RSA-2048 public key for verifying JWTs. Stored in Secret Manager."
type = string
sensitive = true
}
variable "vault_token" {
description = "HashiCorp Vault token. Leave empty to disable Vault integration."
type = string
sensitive = true
default = ""
}
################################################################################
# Optional configuration
################################################################################
variable "vault_addr" {
description = "HashiCorp Vault server address. Leave empty to disable Vault integration."
type = string
default = ""
}
variable "vault_mount" {
description = "HashiCorp Vault KV v2 mount path."
type = string
default = "secret"
}
variable "cors_origin" {
description = "CORS_ORIGIN value for the app."
type = string
default = "*"
}
variable "cloud_run_min_instances" {
description = "Minimum Cloud Run instances (set > 0 to prevent cold starts)."
type = number
default = 1
}
variable "cloud_run_max_instances" {
description = "Maximum Cloud Run instances."
type = number
default = 10
}
variable "cloud_run_cpu" {
description = "CPU limit per Cloud Run instance."
type = string
default = "1"
}
variable "cloud_run_memory" {
description = "Memory limit per Cloud Run instance."
type = string
default = "512Mi"
}
variable "memorystore_memory_size_gb" {
description = "Memory size in GiB for the Memorystore Redis instance."
type = number
default = 1
}
variable "memorystore_redis_version" {
description = "Redis version for Memorystore."
type = string
default = "REDIS_7_0"
}
variable "memorystore_tier" {
description = "Memorystore service tier: BASIC (single node) or STANDARD_HA (primary + replica)."
type = string
default = "STANDARD_HA"
}
variable "deletion_protection" {
description = "Enable deletion protection on Cloud SQL and Memorystore resources."
type = bool
default = true
}