Files
infrastructure/servers/gcp/load-balancer.tf

294 lines
9.5 KiB
Terraform

# ── 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
}
}
# ── 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
}
}
# ── 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",
]
}
}
# ── Prod URL Map — host-based routing ────────────────────────────────────────
# One global IP handles all three services via Host header routing.
# - neurontechnologies.ai / www. → marketing (default)
# - api.neurontechnologies.ai → API backend
# - accounts.neurontechnologies.ai → accounts backend
resource "google_compute_url_map" "prod" {
name = "marketing-urlmap-prod"
project = var.project_id
default_service = google_compute_backend_service.prod.self_link
host_rule {
hosts = ["neurontechnologies.ai", "www.neurontechnologies.ai"]
path_matcher = "marketing"
}
host_rule {
hosts = ["api.neurontechnologies.ai"]
path_matcher = "api"
}
host_rule {
hosts = ["accounts.neurontechnologies.ai"]
path_matcher = "accounts"
}
host_rule {
hosts = ["neuron.neurontechnologies.ai", "*.neurontechnologies.ai"]
path_matcher = "soma"
}
path_matcher {
name = "marketing"
default_service = google_compute_backend_service.prod.self_link
}
path_matcher {
name = "api"
default_service = google_compute_backend_service.api.self_link
}
path_matcher {
name = "accounts"
default_service = google_compute_backend_service.accounts.self_link
}
path_matcher {
name = "soma"
default_service = google_compute_backend_service.soma.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,
google_compute_managed_ssl_certificate.accounts.self_link,
google_compute_managed_ssl_certificate.api.self_link,
google_compute_managed_ssl_certificate.soma.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"
}