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:
477
terraform/environments/gcp/main.tf
Normal file
477
terraform/environments/gcp/main.tf
Normal 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,
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user