From 91bfc429893a3020e3908c55888a08fcdb99dbea Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Fri, 24 Apr 2026 20:07:41 -0500 Subject: [PATCH] =?UTF-8?q?ops:=20scale=20down=20all=20non-critical=20serv?= =?UTF-8?q?ices=20=E2=80=94=20dedicate=20resources=20to=20Neuron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- servers/gcp/cloud-run.tf | 467 ++++++++++++++++++ servers/gcp/load-balancer.tf | 349 +++++++++++++ servers/gcp/main.tf | 37 ++ servers/gcp/outputs.tf | 44 ++ servers/gcp/service-account.tf | 54 ++ servers/gcp/terraform.tfvars | 1 + servers/gcp/variables.tf | 43 ++ servers/legion/apps/ollama.yaml | 2 +- servers/legion/k8s/docuseal/deployment.yaml | 2 +- .../k8s/fornax/coordinator/deployment.yaml | 2 +- servers/legion/k8s/fornax/ui/deployment.yaml | 2 +- .../k8s/fornax/worker/base/deployment.yaml | 2 +- .../k8s/harmonic-wordpress/deployment.yaml | 4 +- servers/legion/k8s/listmonk/deployment.yaml | 2 +- servers/legion/k8s/media/bazarr.yaml | 2 +- servers/legion/k8s/media/flaresolverr.yaml | 2 +- servers/legion/k8s/media/fornax-workers.yaml | 4 +- servers/legion/k8s/media/overseerr.yaml | 2 +- servers/legion/k8s/media/plex.yaml | 2 +- servers/legion/k8s/media/prowlarr.yaml | 2 +- servers/legion/k8s/media/radarr.yaml | 2 +- servers/legion/k8s/media/sonarr.yaml | 2 +- servers/legion/k8s/memos/deployment.yaml | 2 +- servers/legion/k8s/plane/api.yaml | 2 +- servers/legion/k8s/plane/beat.yaml | 2 +- servers/legion/k8s/plane/postgres.yaml | 2 +- servers/legion/k8s/plane/redis.yaml | 2 +- servers/legion/k8s/plane/web.yaml | 2 +- servers/legion/k8s/plane/worker.yaml | 2 +- servers/legion/k8s/redpanda/deployment.yaml | 4 +- .../legion/k8s/wp-coordinator/deployment.yaml | 2 +- 31 files changed, 1022 insertions(+), 27 deletions(-) create mode 100644 servers/gcp/cloud-run.tf create mode 100644 servers/gcp/load-balancer.tf create mode 100644 servers/gcp/main.tf create mode 100644 servers/gcp/outputs.tf create mode 100644 servers/gcp/service-account.tf create mode 100644 servers/gcp/terraform.tfvars create mode 100644 servers/gcp/variables.tf diff --git a/servers/gcp/cloud-run.tf b/servers/gcp/cloud-run.tf new file mode 100644 index 0000000..660679d --- /dev/null +++ b/servers/gcp/cloud-run.tf @@ -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] +} diff --git a/servers/gcp/load-balancer.tf b/servers/gcp/load-balancer.tf new file mode 100644 index 0000000..9a6c345 --- /dev/null +++ b/servers/gcp/load-balancer.tf @@ -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" +} diff --git a/servers/gcp/main.tf b/servers/gcp/main.tf new file mode 100644 index 0000000..702fab2 --- /dev/null +++ b/servers/gcp/main.tf @@ -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" +} diff --git a/servers/gcp/outputs.tf b/servers/gcp/outputs.tf new file mode 100644 index 0000000..5613db7 --- /dev/null +++ b/servers/gcp/outputs.tf @@ -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 + } +} diff --git a/servers/gcp/service-account.tf b/servers/gcp/service-account.tf new file mode 100644 index 0000000..70fcc90 --- /dev/null +++ b/servers/gcp/service-account.tf @@ -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. +} diff --git a/servers/gcp/terraform.tfvars b/servers/gcp/terraform.tfvars new file mode 100644 index 0000000..ced3ef0 --- /dev/null +++ b/servers/gcp/terraform.tfvars @@ -0,0 +1 @@ +cloudflare_zone_id_neurontechnologies = "e844374f203dca4944d77d40ca0710ae" diff --git a/servers/gcp/variables.tf b/servers/gcp/variables.tf new file mode 100644 index 0000000..dae6e0b --- /dev/null +++ b/servers/gcp/variables.tf @@ -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: " +} + +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" +} diff --git a/servers/legion/apps/ollama.yaml b/servers/legion/apps/ollama.yaml index 6618fb8..ed2e2ba 100644 --- a/servers/legion/apps/ollama.yaml +++ b/servers/legion/apps/ollama.yaml @@ -6,7 +6,7 @@ metadata: labels: app: ollama spec: - replicas: 1 + replicas: 0 strategy: type: Recreate selector: diff --git a/servers/legion/k8s/docuseal/deployment.yaml b/servers/legion/k8s/docuseal/deployment.yaml index 75ca1f3..21b170f 100644 --- a/servers/legion/k8s/docuseal/deployment.yaml +++ b/servers/legion/k8s/docuseal/deployment.yaml @@ -4,7 +4,7 @@ metadata: name: docuseal namespace: docuseal spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: docuseal diff --git a/servers/legion/k8s/fornax/coordinator/deployment.yaml b/servers/legion/k8s/fornax/coordinator/deployment.yaml index 0fe3dfb..db95fb0 100644 --- a/servers/legion/k8s/fornax/coordinator/deployment.yaml +++ b/servers/legion/k8s/fornax/coordinator/deployment.yaml @@ -6,7 +6,7 @@ metadata: labels: app: fornax-coordinator spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: fornax-coordinator diff --git a/servers/legion/k8s/fornax/ui/deployment.yaml b/servers/legion/k8s/fornax/ui/deployment.yaml index febdeb3..c520c7d 100644 --- a/servers/legion/k8s/fornax/ui/deployment.yaml +++ b/servers/legion/k8s/fornax/ui/deployment.yaml @@ -6,7 +6,7 @@ metadata: labels: app: fornax-ui spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: fornax-ui diff --git a/servers/legion/k8s/fornax/worker/base/deployment.yaml b/servers/legion/k8s/fornax/worker/base/deployment.yaml index 23a383d..55f9cdf 100644 --- a/servers/legion/k8s/fornax/worker/base/deployment.yaml +++ b/servers/legion/k8s/fornax/worker/base/deployment.yaml @@ -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 diff --git a/servers/legion/k8s/harmonic-wordpress/deployment.yaml b/servers/legion/k8s/harmonic-wordpress/deployment.yaml index 37794c4..147d434 100644 --- a/servers/legion/k8s/harmonic-wordpress/deployment.yaml +++ b/servers/legion/k8s/harmonic-wordpress/deployment.yaml @@ -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: diff --git a/servers/legion/k8s/listmonk/deployment.yaml b/servers/legion/k8s/listmonk/deployment.yaml index 62b2b9a..4f66494 100644 --- a/servers/legion/k8s/listmonk/deployment.yaml +++ b/servers/legion/k8s/listmonk/deployment.yaml @@ -5,7 +5,7 @@ metadata: name: listmonk namespace: listmonk spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: listmonk diff --git a/servers/legion/k8s/media/bazarr.yaml b/servers/legion/k8s/media/bazarr.yaml index a6502c6..1e0326a 100644 --- a/servers/legion/k8s/media/bazarr.yaml +++ b/servers/legion/k8s/media/bazarr.yaml @@ -114,7 +114,7 @@ metadata: labels: app: bazarr spec: - replicas: 1 + replicas: 0 strategy: type: Recreate selector: diff --git a/servers/legion/k8s/media/flaresolverr.yaml b/servers/legion/k8s/media/flaresolverr.yaml index 9067029..4d9bda1 100644 --- a/servers/legion/k8s/media/flaresolverr.yaml +++ b/servers/legion/k8s/media/flaresolverr.yaml @@ -9,7 +9,7 @@ metadata: labels: app: flaresolverr spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: flaresolverr diff --git a/servers/legion/k8s/media/fornax-workers.yaml b/servers/legion/k8s/media/fornax-workers.yaml index 7371850..092f4ea 100644 --- a/servers/legion/k8s/media/fornax-workers.yaml +++ b/servers/legion/k8s/media/fornax-workers.yaml @@ -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: diff --git a/servers/legion/k8s/media/overseerr.yaml b/servers/legion/k8s/media/overseerr.yaml index 1510185..ea63b11 100644 --- a/servers/legion/k8s/media/overseerr.yaml +++ b/servers/legion/k8s/media/overseerr.yaml @@ -133,7 +133,7 @@ metadata: labels: app: overseerr spec: - replicas: 1 + replicas: 0 strategy: type: Recreate selector: diff --git a/servers/legion/k8s/media/plex.yaml b/servers/legion/k8s/media/plex.yaml index b939b82..cdd6195 100644 --- a/servers/legion/k8s/media/plex.yaml +++ b/servers/legion/k8s/media/plex.yaml @@ -17,7 +17,7 @@ metadata: labels: app: jellyfin spec: - replicas: 1 + replicas: 0 strategy: type: Recreate selector: diff --git a/servers/legion/k8s/media/prowlarr.yaml b/servers/legion/k8s/media/prowlarr.yaml index c509805..f6b4aef 100644 --- a/servers/legion/k8s/media/prowlarr.yaml +++ b/servers/legion/k8s/media/prowlarr.yaml @@ -15,7 +15,7 @@ metadata: labels: app: prowlarr spec: - replicas: 1 + replicas: 0 strategy: type: Recreate selector: diff --git a/servers/legion/k8s/media/radarr.yaml b/servers/legion/k8s/media/radarr.yaml index 638e174..5dae511 100644 --- a/servers/legion/k8s/media/radarr.yaml +++ b/servers/legion/k8s/media/radarr.yaml @@ -10,7 +10,7 @@ metadata: labels: app: radarr spec: - replicas: 1 + replicas: 0 strategy: type: Recreate selector: diff --git a/servers/legion/k8s/media/sonarr.yaml b/servers/legion/k8s/media/sonarr.yaml index ad01698..a83fc19 100644 --- a/servers/legion/k8s/media/sonarr.yaml +++ b/servers/legion/k8s/media/sonarr.yaml @@ -11,7 +11,7 @@ metadata: labels: app: sonarr spec: - replicas: 1 + replicas: 0 strategy: type: Recreate selector: diff --git a/servers/legion/k8s/memos/deployment.yaml b/servers/legion/k8s/memos/deployment.yaml index 47ee171..066b849 100644 --- a/servers/legion/k8s/memos/deployment.yaml +++ b/servers/legion/k8s/memos/deployment.yaml @@ -5,7 +5,7 @@ metadata: name: memos namespace: memos spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: memos diff --git a/servers/legion/k8s/plane/api.yaml b/servers/legion/k8s/plane/api.yaml index a25c67d..65c3630 100644 --- a/servers/legion/k8s/plane/api.yaml +++ b/servers/legion/k8s/plane/api.yaml @@ -5,7 +5,7 @@ metadata: name: plane-api namespace: plane spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: plane-api diff --git a/servers/legion/k8s/plane/beat.yaml b/servers/legion/k8s/plane/beat.yaml index 066de27..4f669f2 100644 --- a/servers/legion/k8s/plane/beat.yaml +++ b/servers/legion/k8s/plane/beat.yaml @@ -4,7 +4,7 @@ metadata: name: plane-beat namespace: plane spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: plane-beat diff --git a/servers/legion/k8s/plane/postgres.yaml b/servers/legion/k8s/plane/postgres.yaml index 7cf2434..7ae8d50 100644 --- a/servers/legion/k8s/plane/postgres.yaml +++ b/servers/legion/k8s/plane/postgres.yaml @@ -17,7 +17,7 @@ metadata: name: plane-postgres namespace: plane spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: plane-postgres diff --git a/servers/legion/k8s/plane/redis.yaml b/servers/legion/k8s/plane/redis.yaml index e88e0d4..2f6ce83 100644 --- a/servers/legion/k8s/plane/redis.yaml +++ b/servers/legion/k8s/plane/redis.yaml @@ -5,7 +5,7 @@ metadata: name: plane-redis namespace: plane spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: plane-redis diff --git a/servers/legion/k8s/plane/web.yaml b/servers/legion/k8s/plane/web.yaml index bd16975..755d462 100644 --- a/servers/legion/k8s/plane/web.yaml +++ b/servers/legion/k8s/plane/web.yaml @@ -5,7 +5,7 @@ metadata: name: plane-web namespace: plane spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: plane-web diff --git a/servers/legion/k8s/plane/worker.yaml b/servers/legion/k8s/plane/worker.yaml index 39c96ca..e68bd55 100644 --- a/servers/legion/k8s/plane/worker.yaml +++ b/servers/legion/k8s/plane/worker.yaml @@ -4,7 +4,7 @@ metadata: name: plane-worker namespace: plane spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: plane-worker diff --git a/servers/legion/k8s/redpanda/deployment.yaml b/servers/legion/k8s/redpanda/deployment.yaml index d04169d..e34d538 100644 --- a/servers/legion/k8s/redpanda/deployment.yaml +++ b/servers/legion/k8s/redpanda/deployment.yaml @@ -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 diff --git a/servers/legion/k8s/wp-coordinator/deployment.yaml b/servers/legion/k8s/wp-coordinator/deployment.yaml index 49a4dc4..a187ca6 100644 --- a/servers/legion/k8s/wp-coordinator/deployment.yaml +++ b/servers/legion/k8s/wp-coordinator/deployment.yaml @@ -6,7 +6,7 @@ metadata: labels: app: wp-coordinator spec: - replicas: 1 + replicas: 0 selector: matchLabels: app: wp-coordinator