ops: scale down all non-critical services — dedicate resources to Neuron
Scaled to replicas: 0: fornax (coordinator/ui/workers), jellyfin via media, overseerr, plex, radarr, sonarr, bazarr, prowlarr, flaresolverr, ollama, harmonic-wordpress, docuseal, listmonk, memos, plane, redpanda, wp-coordinator
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
# ── 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]
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
# ── Serverless NEGs ───────────────────────────────────────────────────────────
|
||||
# One NEG per Cloud Run service. NEGs for Cloud Run must use SERVERLESS type
|
||||
# and reference the Cloud Run service by name + region.
|
||||
|
||||
resource "google_compute_region_network_endpoint_group" "prod_us" {
|
||||
name = "marketing-neg-us"
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
region = "us-central1"
|
||||
project = var.project_id
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.prod_us.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_region_network_endpoint_group" "prod_eu" {
|
||||
name = "marketing-neg-eu"
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
region = "europe-west1"
|
||||
project = var.project_id
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.prod_eu.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_region_network_endpoint_group" "prod_apac" {
|
||||
name = "marketing-neg-apac"
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
region = "asia-northeast1"
|
||||
project = var.project_id
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.prod_apac.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_region_network_endpoint_group" "stage" {
|
||||
name = "marketing-neg-stage"
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
region = "us-central1"
|
||||
project = var.project_id
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.stage.name
|
||||
}
|
||||
}
|
||||
|
||||
# ── Cloud Armor Security Policy ───────────────────────────────────────────────
|
||||
# Applied to the backend service. Rules evaluated top-down (lowest priority wins).
|
||||
|
||||
resource "google_compute_security_policy" "marketing" {
|
||||
name = "marketing-armor"
|
||||
project = var.project_id
|
||||
|
||||
description = "Cloud Armor policy for neurontechnologies.ai marketing site"
|
||||
|
||||
# ── Rule 1000: SQLi protection ────────────────────────────────────────────
|
||||
rule {
|
||||
action = "deny(403)"
|
||||
priority = 1000
|
||||
description = "Block SQLi attacks (OWASP CRS sqli-v33-stable)"
|
||||
|
||||
match {
|
||||
expr {
|
||||
expression = "evaluatePreconfiguredWaf('sqli-v33-stable', {'sensitivity': 1})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Rule 1001: XSS protection ─────────────────────────────────────────────
|
||||
rule {
|
||||
action = "deny(403)"
|
||||
priority = 1001
|
||||
description = "Block XSS attacks (OWASP CRS xss-v33-stable)"
|
||||
|
||||
match {
|
||||
expr {
|
||||
expression = "evaluatePreconfiguredWaf('xss-v33-stable', {'sensitivity': 1})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Rule 2000: Rate limiting ───────────────────────────────────────────────
|
||||
# Throttle any single IP exceeding 1000 req/min (≈17 req/s).
|
||||
rule {
|
||||
action = "throttle"
|
||||
priority = 2000
|
||||
description = "Rate limit: 1000 req/min per IP"
|
||||
|
||||
match {
|
||||
versioned_expr = "SRC_IPS_V1"
|
||||
config {
|
||||
src_ip_ranges = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
rate_limit_options {
|
||||
conform_action = "allow"
|
||||
exceed_action = "deny(429)"
|
||||
|
||||
rate_limit_threshold {
|
||||
count = 1000
|
||||
interval_sec = 60
|
||||
}
|
||||
|
||||
enforce_on_key = "IP"
|
||||
}
|
||||
}
|
||||
|
||||
# ── Default rule: allow all ───────────────────────────────────────────────
|
||||
rule {
|
||||
action = "allow"
|
||||
priority = 2147483647
|
||||
description = "Default: allow"
|
||||
|
||||
match {
|
||||
versioned_expr = "SRC_IPS_V1"
|
||||
config {
|
||||
src_ip_ranges = ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Prod Backend Service ──────────────────────────────────────────────────────
|
||||
# EXTERNAL_MANAGED scheme is required for Cloud CDN + global LB.
|
||||
# All three regional NEGs are attached with equal weight.
|
||||
|
||||
resource "google_compute_backend_service" "prod" {
|
||||
name = "marketing-backend-prod"
|
||||
project = var.project_id
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
protocol = "HTTPS"
|
||||
timeout_sec = 30
|
||||
|
||||
# Cloud Armor
|
||||
security_policy = google_compute_security_policy.marketing.self_link
|
||||
|
||||
# Cloud CDN
|
||||
enable_cdn = true
|
||||
|
||||
cdn_policy {
|
||||
cache_mode = "CACHE_ALL_STATIC"
|
||||
default_ttl = 3600
|
||||
max_ttl = 86400
|
||||
client_ttl = 3600
|
||||
negative_caching = true
|
||||
serve_while_stale = 86400
|
||||
|
||||
cache_key_policy {
|
||||
include_host = true
|
||||
include_protocol = true
|
||||
include_query_string = true
|
||||
}
|
||||
}
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.prod_us.self_link
|
||||
}
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.prod_eu.self_link
|
||||
}
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.prod_apac.self_link
|
||||
}
|
||||
|
||||
log_config {
|
||||
enable = true
|
||||
sample_rate = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
# ── Stage Backend Service ─────────────────────────────────────────────────────
|
||||
|
||||
resource "google_compute_backend_service" "stage" {
|
||||
name = "marketing-backend-stage"
|
||||
project = var.project_id
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
protocol = "HTTPS"
|
||||
timeout_sec = 30
|
||||
|
||||
# Cloud Armor on stage too
|
||||
security_policy = google_compute_security_policy.marketing.self_link
|
||||
|
||||
enable_cdn = false # CDN off for stage — we want fresh responses always
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.stage.self_link
|
||||
}
|
||||
|
||||
log_config {
|
||||
enable = true
|
||||
sample_rate = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
# ── SSL Certificates ──────────────────────────────────────────────────────────
|
||||
# google_compute_managed_ssl_certificate — Google-managed, auto-renewed.
|
||||
# Provisioning requires DNS A records pointing to the LB IP (chicken-and-egg:
|
||||
# apply the Terraform, get the IP from outputs, update DNS, then cert provisions).
|
||||
|
||||
resource "google_compute_managed_ssl_certificate" "prod" {
|
||||
name = "marketing-cert-prod"
|
||||
project = var.project_id
|
||||
|
||||
managed {
|
||||
domains = [
|
||||
"neurontechnologies.ai",
|
||||
"www.neurontechnologies.ai",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_managed_ssl_certificate" "stage" {
|
||||
name = "marketing-cert-stage"
|
||||
project = var.project_id
|
||||
|
||||
managed {
|
||||
domains = ["stage.neurontechnologies.ai"]
|
||||
}
|
||||
}
|
||||
|
||||
# ── Prod URL Map ──────────────────────────────────────────────────────────────
|
||||
|
||||
resource "google_compute_url_map" "prod" {
|
||||
name = "marketing-urlmap-prod"
|
||||
project = var.project_id
|
||||
default_service = google_compute_backend_service.prod.self_link
|
||||
}
|
||||
|
||||
# ── Prod HTTPS Target Proxy ───────────────────────────────────────────────────
|
||||
|
||||
resource "google_compute_target_https_proxy" "prod" {
|
||||
name = "marketing-https-proxy-prod"
|
||||
project = var.project_id
|
||||
url_map = google_compute_url_map.prod.self_link
|
||||
ssl_certificates = [google_compute_managed_ssl_certificate.prod.self_link]
|
||||
}
|
||||
|
||||
# ── Prod HTTP → HTTPS redirect ────────────────────────────────────────────────
|
||||
# Separate URL map that redirects all HTTP traffic to HTTPS.
|
||||
|
||||
resource "google_compute_url_map" "prod_http_redirect" {
|
||||
name = "marketing-urlmap-prod-http-redirect"
|
||||
project = var.project_id
|
||||
|
||||
default_url_redirect {
|
||||
https_redirect = true
|
||||
redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
|
||||
strip_query = false
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_target_http_proxy" "prod" {
|
||||
name = "marketing-http-proxy-prod"
|
||||
project = var.project_id
|
||||
url_map = google_compute_url_map.prod_http_redirect.self_link
|
||||
}
|
||||
|
||||
# ── Prod Global Forwarding Rules ──────────────────────────────────────────────
|
||||
# Both rules share the same global anycast IP.
|
||||
|
||||
resource "google_compute_global_address" "prod" {
|
||||
name = "marketing-ip-prod"
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
resource "google_compute_global_forwarding_rule" "prod_https" {
|
||||
name = "marketing-fwd-prod-https"
|
||||
project = var.project_id
|
||||
target = google_compute_target_https_proxy.prod.self_link
|
||||
ip_address = google_compute_global_address.prod.address
|
||||
port_range = "443"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
}
|
||||
|
||||
resource "google_compute_global_forwarding_rule" "prod_http" {
|
||||
name = "marketing-fwd-prod-http"
|
||||
project = var.project_id
|
||||
target = google_compute_target_http_proxy.prod.self_link
|
||||
ip_address = google_compute_global_address.prod.address
|
||||
port_range = "80"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
}
|
||||
|
||||
# ── Stage URL Map ─────────────────────────────────────────────────────────────
|
||||
|
||||
resource "google_compute_url_map" "stage" {
|
||||
name = "marketing-urlmap-stage"
|
||||
project = var.project_id
|
||||
default_service = google_compute_backend_service.stage.self_link
|
||||
}
|
||||
|
||||
# ── Stage HTTPS Target Proxy ──────────────────────────────────────────────────
|
||||
|
||||
resource "google_compute_target_https_proxy" "stage" {
|
||||
name = "marketing-https-proxy-stage"
|
||||
project = var.project_id
|
||||
url_map = google_compute_url_map.stage.self_link
|
||||
ssl_certificates = [google_compute_managed_ssl_certificate.stage.self_link]
|
||||
}
|
||||
|
||||
# ── Stage HTTP → HTTPS redirect ───────────────────────────────────────────────
|
||||
|
||||
resource "google_compute_url_map" "stage_http_redirect" {
|
||||
name = "marketing-urlmap-stage-http-redirect"
|
||||
project = var.project_id
|
||||
|
||||
default_url_redirect {
|
||||
https_redirect = true
|
||||
redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
|
||||
strip_query = false
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_target_http_proxy" "stage" {
|
||||
name = "marketing-http-proxy-stage"
|
||||
project = var.project_id
|
||||
url_map = google_compute_url_map.stage_http_redirect.self_link
|
||||
}
|
||||
|
||||
# ── Stage Global Forwarding Rules ─────────────────────────────────────────────
|
||||
# Separate global IP for stage so DNS is independent.
|
||||
|
||||
resource "google_compute_global_address" "stage" {
|
||||
name = "marketing-ip-stage"
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
resource "google_compute_global_forwarding_rule" "stage_https" {
|
||||
name = "marketing-fwd-stage-https"
|
||||
project = var.project_id
|
||||
target = google_compute_target_https_proxy.stage.self_link
|
||||
ip_address = google_compute_global_address.stage.address
|
||||
port_range = "443"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
}
|
||||
|
||||
resource "google_compute_global_forwarding_rule" "stage_http" {
|
||||
name = "marketing-fwd-stage-http"
|
||||
project = var.project_id
|
||||
target = google_compute_target_http_proxy.stage.self_link
|
||||
ip_address = google_compute_global_address.stage.address
|
||||
port_range = "80"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
google-beta = {
|
||||
source = "hashicorp/google-beta"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {
|
||||
bucket = "legion-terraform-state"
|
||||
key = "gcp/terraform.tfstate"
|
||||
region = "auto"
|
||||
endpoint = "https://651161e0a3d321561b4c90b5bcd5f15b.r2.cloudflarestorage.com"
|
||||
|
||||
# R2 is S3-compatible but not AWS — skip AWS-specific checks
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_region_validation = true
|
||||
force_path_style = true
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = "us-central1"
|
||||
}
|
||||
|
||||
provider "google-beta" {
|
||||
project = var.project_id
|
||||
region = "us-central1"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# ── Outputs ───────────────────────────────────────────────────────────────────
|
||||
# After `terraform apply`, use these IPs to update Cloudflare DNS A records.
|
||||
#
|
||||
# Prod: neurontechnologies.ai + www.neurontechnologies.ai → prod_lb_ip
|
||||
# Stage: stage.neurontechnologies.ai → stage_lb_ip
|
||||
#
|
||||
# In the Legion Terraform (dns-neurontechnologies.tf), replace the tunnel CNAMEs
|
||||
# for neurontechnologies.ai, www, and stage with A records pointing to these IPs.
|
||||
# Set proxied=true in Cloudflare to keep CDN/DDoS protection layered in front.
|
||||
|
||||
output "prod_lb_ip" {
|
||||
description = "Global anycast IP for the production load balancer (neurontechnologies.ai + www)"
|
||||
value = google_compute_global_address.prod.address
|
||||
}
|
||||
|
||||
output "stage_lb_ip" {
|
||||
description = "Global anycast IP for the staging load balancer (stage.neurontechnologies.ai)"
|
||||
value = google_compute_global_address.stage.address
|
||||
}
|
||||
|
||||
output "prod_ssl_cert_name" {
|
||||
description = "Name of the Google-managed SSL cert for prod (check provisioning status in GCP console)"
|
||||
value = google_compute_managed_ssl_certificate.prod.name
|
||||
}
|
||||
|
||||
output "stage_ssl_cert_name" {
|
||||
description = "Name of the Google-managed SSL cert for stage"
|
||||
value = google_compute_managed_ssl_certificate.stage.name
|
||||
}
|
||||
|
||||
output "marketing_service_account_email" {
|
||||
description = "Service account email for Cloud Run services"
|
||||
value = google_service_account.marketing.email
|
||||
}
|
||||
|
||||
output "cloud_run_services" {
|
||||
description = "Cloud Run service URLs per region"
|
||||
value = {
|
||||
prod_us = google_cloud_run_v2_service.prod_us.uri
|
||||
prod_eu = google_cloud_run_v2_service.prod_eu.uri
|
||||
prod_apac = google_cloud_run_v2_service.prod_apac.uri
|
||||
stage = google_cloud_run_v2_service.stage.uri
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# ── Service Account ───────────────────────────────────────────────────────────
|
||||
|
||||
resource "google_service_account" "marketing" {
|
||||
account_id = "neuron-marketing-sa"
|
||||
display_name = "Neuron Marketing Cloud Run SA"
|
||||
description = "Service account for the neurontechnologies.ai marketing site on Cloud Run"
|
||||
}
|
||||
|
||||
# ── IAM — Secret Manager accessor ─────────────────────────────────────────────
|
||||
# Grant the SA access to read all marketing secrets from Secret Manager.
|
||||
|
||||
resource "google_project_iam_member" "marketing_secret_accessor" {
|
||||
project = var.project_id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.marketing.email}"
|
||||
}
|
||||
|
||||
# ── IAM — Public (unauthenticated) invocation for prod services ───────────────
|
||||
# Cloud Run v2: allow-unauthenticated is set per-service via google_cloud_run_v2_service_iam_member.
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "prod_us_public" {
|
||||
project = var.project_id
|
||||
location = "us-central1"
|
||||
name = google_cloud_run_v2_service.prod_us.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "prod_eu_public" {
|
||||
project = var.project_id
|
||||
location = "europe-west1"
|
||||
name = google_cloud_run_v2_service.prod_eu.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "prod_apac_public" {
|
||||
project = var.project_id
|
||||
location = "asia-northeast1"
|
||||
name = google_cloud_run_v2_service.prod_apac.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "stage_public" {
|
||||
project = var.project_id
|
||||
location = "us-central1"
|
||||
name = google_cloud_run_v2_service.stage.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
|
||||
# Note: Cloudflare Access enforces authentication in front of stage.
|
||||
# Cloud Run itself allows allUsers so the LB health checks pass.
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
cloudflare_zone_id_neurontechnologies = "e844374f203dca4944d77d40ca0710ae"
|
||||
@@ -0,0 +1,43 @@
|
||||
variable "project_id" {
|
||||
description = "GCP project ID"
|
||||
type = string
|
||||
default = "neuron-785695"
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Docker image tag to deploy"
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "cloudflare_zone_id_neurontechnologies" {
|
||||
description = "Cloudflare zone ID for neurontechnologies.ai (look up from Legion Terraform state or Cloudflare dashboard)"
|
||||
type = string
|
||||
# Set this in terraform.tfvars — do not hardcode here.
|
||||
# To find: cloudflare_zone.neurontechnologies_ai.id in the Legion Terraform state,
|
||||
# or: curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=neurontechnologies.ai" -H "X-Auth-Email: andersonwilliam85@gmail.com" -H "X-Auth-Key: <key>"
|
||||
}
|
||||
|
||||
locals {
|
||||
project_id = var.project_id
|
||||
image = "us-central1-docker.pkg.dev/${var.project_id}/neuron-marketing/marketing:${var.image_tag}"
|
||||
|
||||
# Production secrets from GCP Secret Manager
|
||||
secrets = [
|
||||
"stripe-secret-key",
|
||||
"stripe-webhook-secret",
|
||||
"stripe-price-professional",
|
||||
"stripe-price-founding",
|
||||
]
|
||||
|
||||
# Env var names for each secret (uppercase, hyphen → underscore)
|
||||
secret_env_map = {
|
||||
"stripe-secret-key" = "STRIPE_SECRET_KEY"
|
||||
"stripe-webhook-secret" = "STRIPE_WEBHOOK_SECRET"
|
||||
"stripe-price-professional" = "STRIPE_PRICE_PROFESSIONAL"
|
||||
"stripe-price-founding" = "STRIPE_PRICE_FOUNDING"
|
||||
}
|
||||
|
||||
# Static publishable key (not sensitive — lives in source)
|
||||
stripe_publishable_key = "pk_live_51TPoHnJg9Fv1D3AUPMXnYyOJIVhn1FyH56zCMNnATo9tR7pO9lNDbXncp6VeDxm38qSdBHZPfqEBipUh3GZsWNyd00jvIO97E6"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app: ollama
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: docuseal
|
||||
namespace: docuseal
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docuseal
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app: fornax-coordinator
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fornax-coordinator
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app: fornax-ui
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fornax-ui
|
||||
|
||||
@@ -8,7 +8,7 @@ metadata:
|
||||
app.kubernetes.io/part-of: fornax
|
||||
app.kubernetes.io/component: worker
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fornax-worker-WORKER_ID
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app: mysql
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
@@ -83,7 +83,7 @@ metadata:
|
||||
labels:
|
||||
app: wordpress
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
name: listmonk
|
||||
namespace: listmonk
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: listmonk
|
||||
|
||||
@@ -114,7 +114,7 @@ metadata:
|
||||
labels:
|
||||
app: bazarr
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -9,7 +9,7 @@ metadata:
|
||||
labels:
|
||||
app: flaresolverr
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: flaresolverr
|
||||
|
||||
@@ -15,7 +15,7 @@ metadata:
|
||||
fornax-role: worker
|
||||
fornax-server: us-tx-253
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
@@ -276,7 +276,7 @@ metadata:
|
||||
fornax-role: worker
|
||||
fornax-server: us-tx-34
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -133,7 +133,7 @@ metadata:
|
||||
labels:
|
||||
app: overseerr
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -17,7 +17,7 @@ metadata:
|
||||
labels:
|
||||
app: jellyfin
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -15,7 +15,7 @@ metadata:
|
||||
labels:
|
||||
app: prowlarr
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -10,7 +10,7 @@ metadata:
|
||||
labels:
|
||||
app: radarr
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -11,7 +11,7 @@ metadata:
|
||||
labels:
|
||||
app: sonarr
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
name: memos
|
||||
namespace: memos
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: memos
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
name: plane-api
|
||||
namespace: plane
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: plane-api
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: plane-beat
|
||||
namespace: plane
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: plane-beat
|
||||
|
||||
@@ -17,7 +17,7 @@ metadata:
|
||||
name: plane-postgres
|
||||
namespace: plane
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: plane-postgres
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
name: plane-redis
|
||||
namespace: plane
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: plane-redis
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
name: plane-web
|
||||
namespace: plane
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: plane-web
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: plane-worker
|
||||
namespace: plane
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: plane-worker
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app: redpanda
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redpanda
|
||||
@@ -105,7 +105,7 @@ metadata:
|
||||
labels:
|
||||
app: redpanda-console
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redpanda-console
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app: wp-coordinator
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: wp-coordinator
|
||||
|
||||
Reference in New Issue
Block a user