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>
478 lines
14 KiB
HCL
478 lines
14 KiB
HCL
################################################################################
|
|
# 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,
|
|
]
|
|
}
|