# ── Cloud Run v2 Services ───────────────────────────────────────────────────── # Three prod regions + one stage region. # All services use INGRESS_TRAFFIC_ALL so the global LB can reach them. # Secrets are mounted as env vars via Secret Manager volume references. locals { run_labels = { "managed-by" = "terraform" "project" = "neuron-marketing" } } # ── Prod — us-central1 ──────────────────────────────────────────────────────── resource "google_cloud_run_v2_service" "prod_us" { name = "marketing-prod-us" location = "us-central1" project = var.project_id # Allow the global HTTP(S) load balancer to reach this service ingress = "INGRESS_TRAFFIC_ALL" labels = local.run_labels template { service_account = google_service_account.marketing.email scaling { min_instance_count = 1 max_instance_count = 100 } containers { image = local.image resources { limits = { cpu = "2" memory = "1Gi" } # cpu_idle = false keeps CPU allocated at all times (no throttling between requests) cpu_idle = false } # ── Static env vars ─────────────────────────────────────────────────── env { name = "NODE_ENV" value = "production" } env { name = "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" value = local.stripe_publishable_key } # ── Secret env vars from Secret Manager ─────────────────────────────── env { name = "STRIPE_SECRET_KEY" value_source { secret_key_ref { secret = "stripe-secret-key" version = "latest" } } } env { name = "STRIPE_WEBHOOK_SECRET" value_source { secret_key_ref { secret = "stripe-webhook-secret" version = "latest" } } } env { name = "STRIPE_PRICE_PROFESSIONAL" value_source { secret_key_ref { secret = "stripe-price-professional" version = "latest" } } } env { name = "STRIPE_PRICE_FOUNDING" value_source { secret_key_ref { secret = "stripe-price-founding" version = "latest" } } } ports { container_port = 3000 name = "http1" } startup_probe { http_get { path = "/api/health" port = 3000 } initial_delay_seconds = 5 timeout_seconds = 5 period_seconds = 10 failure_threshold = 3 } liveness_probe { http_get { path = "/api/health" port = 3000 } initial_delay_seconds = 15 timeout_seconds = 5 period_seconds = 30 failure_threshold = 3 } } max_instance_request_concurrency = 1000 } traffic { type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" percent = 100 } depends_on = [google_project_iam_member.marketing_secret_accessor] } # ── Prod — europe-west1 ─────────────────────────────────────────────────────── resource "google_cloud_run_v2_service" "prod_eu" { name = "marketing-prod-eu" location = "europe-west1" project = var.project_id ingress = "INGRESS_TRAFFIC_ALL" labels = local.run_labels template { service_account = google_service_account.marketing.email scaling { min_instance_count = 1 max_instance_count = 100 } containers { image = local.image resources { limits = { cpu = "2" memory = "1Gi" } cpu_idle = false } env { name = "NODE_ENV" value = "production" } env { name = "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" value = local.stripe_publishable_key } env { name = "STRIPE_SECRET_KEY" value_source { secret_key_ref { secret = "stripe-secret-key" version = "latest" } } } env { name = "STRIPE_WEBHOOK_SECRET" value_source { secret_key_ref { secret = "stripe-webhook-secret" version = "latest" } } } env { name = "STRIPE_PRICE_PROFESSIONAL" value_source { secret_key_ref { secret = "stripe-price-professional" version = "latest" } } } env { name = "STRIPE_PRICE_FOUNDING" value_source { secret_key_ref { secret = "stripe-price-founding" version = "latest" } } } ports { container_port = 3000 name = "http1" } startup_probe { http_get { path = "/api/health" port = 3000 } initial_delay_seconds = 5 timeout_seconds = 5 period_seconds = 10 failure_threshold = 3 } liveness_probe { http_get { path = "/api/health" port = 3000 } initial_delay_seconds = 15 timeout_seconds = 5 period_seconds = 30 failure_threshold = 3 } } max_instance_request_concurrency = 1000 } traffic { type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" percent = 100 } depends_on = [google_project_iam_member.marketing_secret_accessor] } # ── Prod — asia-northeast1 ──────────────────────────────────────────────────── resource "google_cloud_run_v2_service" "prod_apac" { name = "marketing-prod-apac" location = "asia-northeast1" project = var.project_id ingress = "INGRESS_TRAFFIC_ALL" labels = local.run_labels template { service_account = google_service_account.marketing.email scaling { min_instance_count = 1 max_instance_count = 100 } containers { image = local.image resources { limits = { cpu = "2" memory = "1Gi" } cpu_idle = false } env { name = "NODE_ENV" value = "production" } env { name = "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" value = local.stripe_publishable_key } env { name = "STRIPE_SECRET_KEY" value_source { secret_key_ref { secret = "stripe-secret-key" version = "latest" } } } env { name = "STRIPE_WEBHOOK_SECRET" value_source { secret_key_ref { secret = "stripe-webhook-secret" version = "latest" } } } env { name = "STRIPE_PRICE_PROFESSIONAL" value_source { secret_key_ref { secret = "stripe-price-professional" version = "latest" } } } env { name = "STRIPE_PRICE_FOUNDING" value_source { secret_key_ref { secret = "stripe-price-founding" version = "latest" } } } ports { container_port = 3000 name = "http1" } startup_probe { http_get { path = "/api/health" port = 3000 } initial_delay_seconds = 5 timeout_seconds = 5 period_seconds = 10 failure_threshold = 3 } liveness_probe { http_get { path = "/api/health" port = 3000 } initial_delay_seconds = 15 timeout_seconds = 5 period_seconds = 30 failure_threshold = 3 } } max_instance_request_concurrency = 1000 } traffic { type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" percent = 100 } depends_on = [google_project_iam_member.marketing_secret_accessor] } # ── Stage — us-central1 ─────────────────────────────────────────────────────── # min_instances=0 to save cost on staging. # Cloudflare Access handles auth in front; Cloud Run itself is open so the LB # health checks pass without credentials. resource "google_cloud_run_v2_service" "stage" { name = "marketing-stage" location = "us-central1" project = var.project_id ingress = "INGRESS_TRAFFIC_ALL" labels = merge(local.run_labels, { "environment" = "stage" }) template { service_account = google_service_account.marketing.email scaling { min_instance_count = 0 max_instance_count = 10 } containers { image = local.image resources { limits = { cpu = "1" memory = "512Mi" } cpu_idle = true } env { name = "NODE_ENV" value = "production" } env { name = "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" value = local.stripe_publishable_key } env { name = "STRIPE_SECRET_KEY" value_source { secret_key_ref { secret = "stripe-secret-key" version = "latest" } } } env { name = "STRIPE_WEBHOOK_SECRET" value_source { secret_key_ref { secret = "stripe-webhook-secret" version = "latest" } } } env { name = "STRIPE_PRICE_PROFESSIONAL" value_source { secret_key_ref { secret = "stripe-price-professional" version = "latest" } } } env { name = "STRIPE_PRICE_FOUNDING" value_source { secret_key_ref { secret = "stripe-price-founding" version = "latest" } } } ports { container_port = 3000 name = "http1" } startup_probe { http_get { path = "/api/health" port = 3000 } initial_delay_seconds = 5 timeout_seconds = 5 period_seconds = 10 failure_threshold = 5 } } max_instance_request_concurrency = 1000 } traffic { type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" percent = 100 } depends_on = [google_project_iam_member.marketing_secret_accessor] }