diff --git a/envs/gcp/prod/main.tf b/envs/gcp/prod/main.tf index b14e359..64d60f9 100755 --- a/envs/gcp/prod/main.tf +++ b/envs/gcp/prod/main.tf @@ -8,6 +8,29 @@ locals { enable_private_networking = var.enable_private_networking vpc_connector_name = var.vpc_connector_name != "" ? var.vpc_connector_name : "${var.name_prefix}-cr-conn" + + enable_lb = var.enable_lb + + lb_routing_plan = { + domain = var.lb_domain + default_backend = (var.ui_service_name != "" ? "ui" : "backend") + backends = { + backend = { + cloud_run_service = var.app_service_name + region = var.region + } + ui = { + cloud_run_service = var.ui_service_name + region = var.region + } + } + path_routes = [ + for p in var.lb_api_path_prefixes : { + path_prefix = p + backend = "backend" + } + ] + } } # GKE related modules disabled/removed, Cloud Run introduced for app service @@ -162,6 +185,19 @@ module "ui_cloud_run" { vpc_egress = var.cloud_run_vpc_egress } +module "lb_backends" { + count = var.enable_lb ? 1 : 0 + source = "../../../modules/gcp/load_balancer" + + project_id = var.project_id + region = var.region + name_prefix = var.name_prefix + backend_service_name = var.app_service_name + ui_service_name = var.ui_service_name + lb_domain = var.lb_domain + api_path_prefixes = var.lb_api_path_prefixes +} + # Outputs adjusted (removed GKE related ones) output "artifact_registry_repo" { @@ -208,6 +244,16 @@ output "ui_cloud_run_url" { value = local.enable_apps && var.ui_service_name != "" ? module.ui_cloud_run[0].url : null } +output "lb_backend_backend_service" { + description = "Backend service self_link for backend (mono) in the external HTTPS LB" + value = var.enable_lb ? module.lb_backends[0].backend_backend_service_self_link : null +} + +output "lb_ui_backend_service" { + description = "Backend service self_link for UI (Next.js) in the external HTTPS LB" + value = var.enable_lb ? module.lb_backends[0].ui_backend_service_self_link : null +} + output "iam_service_accounts" { description = "Created service accounts with emails and names" value = module.iam.service_accounts @@ -227,3 +273,29 @@ output "monitoring_logging_api_enabled" { description = "Whether Logging/Monitoring APIs are enabled" value = module.monitoring.logging_api_enabled && module.monitoring.monitoring_api_enabled } + +# HTTPS Load Balancer & Routing Strategy (Milestone A) +output "lb_domain" { + description = "The FQDN for the load balancer" + value = var.enable_lb ? local.lb_routing_plan.domain : null +} + +output "lb_ip" { + description = "The public Anycast IP address of the load balancer" + value = var.enable_lb ? module.lb_backends[0].lb_ip : null +} + +output "dns_authorization_record_name" { + description = "DNS CNAME record name for cert verification" + value = var.enable_lb ? module.lb_backends[0].dns_authorization_record_name : null +} + +output "dns_authorization_record_value" { + description = "DNS CNAME record value for cert verification" + value = var.enable_lb ? module.lb_backends[0].dns_authorization_record_value : null +} + +output "lb_routing_plan" { + description = "Detailed routing strategy for the load balancer" + value = var.enable_lb ? local.lb_routing_plan : null +} diff --git a/envs/gcp/prod/terraform.tfvars.example b/envs/gcp/prod/terraform.tfvars.example index 4f7553f..6b631eb 100755 --- a/envs/gcp/prod/terraform.tfvars.example +++ b/envs/gcp/prod/terraform.tfvars.example @@ -70,11 +70,25 @@ enable_redis = true enable_filestore = true enable_apps = true +# Networking mode +# Production recommendation: use private networking (VPC + VPC Connector) and disable public IP. +# Testing option: switch to public IP access (NOT recommended for production). + # Private networking for Cloud SQL / Redis (VPC + VPC Connector) enable_private_networking = true vpc_connector_name = "mega-prod-cr-conn" # vpc_connector_cidr = "10.43.0.0/28" # optional: leave null for auto -cloud_run_vpc_egress = "private-ranges-only" +cloud_run_vpc_egress = "private-ranges-only" + +# Testing: allow connecting to Cloud SQL via public IP (NOT recommended for production). +# Note: Set enable_private_networking above to false if you need to enable public access for testing. +# cloud_sql_enable_private_service_connection = false +# cloud_sql_enable_public_ip = true + +# HTTPS Load Balancer & Routing Strategy (Milestone A) +enable_lb = true +lb_domain = "buck2hub.com" +lb_api_path_prefixes = ["/api/v1", "/info/lfs"] enable_logging = true enable_monitoring = true diff --git a/envs/gcp/prod/variables.tf b/envs/gcp/prod/variables.tf index 6c54b6e..bcd0f57 100755 --- a/envs/gcp/prod/variables.tf +++ b/envs/gcp/prod/variables.tf @@ -1,432 +1,451 @@ -# NOTE: cleaned up variables, removed k8s/ingress legacy blocks - -variable "project_id" { - type = string -} - -variable "region" { - type = string - default = "us-central1" -} - -variable "zone" { - type = string - description = "GCP zone for zonal resources (e.g. Filestore)." - default = "" -} - -variable "name_prefix" { - type = string - default = "mega-prod" -} - -variable "base_domain" { - type = string - default = "" -} - -variable "enable_build_env" { - type = bool - description = "(deprecated) Was used for GKE build env. Default to false after migration to Cloud Run." - default = false -} - -variable "enable_gcs" { - type = bool - default = true -} - -variable "enable_cloud_sql" { - type = bool - default = true -} - -variable "enable_redis" { - type = bool - default = true -} - -variable "enable_filestore" { - type = bool - default = true -} - -variable "enable_apps" { - type = bool - default = true -} - -variable "enable_logging" { - type = bool - default = true -} - -variable "enable_monitoring" { - type = bool - default = true -} - -variable "enable_alerts" { - type = bool - default = true -} - -variable "alert_notification_channels" { - type = list(string) - default = [] - description = "List of notification channel IDs for alerts" -} - -variable "log_sink_name" { - type = string - default = "" -} - -variable "log_sink_destination" { - type = string - default = "" -} - -variable "network_name" { - type = string - default = "mega-prod-net" -} - -variable "subnet_name" { - type = string - default = "mega-prod-subnet" -} - -variable "subnet_cidr" { - type = string - default = "10.40.0.0/16" -} - -variable "pods_secondary_range" { - type = string - default = "10.41.0.0/16" -} - -variable "services_secondary_range" { - type = string - default = "10.42.0.0/16" -} - -# Private networking for Cloud SQL / Redis (Cloud Run -> VPC Connector) -variable "enable_private_networking" { - type = bool - default = true -} - -variable "vpc_connector_name" { - type = string - default = "" -} - -variable "vpc_connector_cidr" { - type = string - default = null -} - -variable "cloud_run_vpc_egress" { - type = string - default = "private-ranges-only" -} - -variable "artifact_registry_location" { - type = string - default = "us-central1" -} - -variable "artifact_registry_repo" { - type = string - description = "Artifact Registry repository name" - default = "mega-prod" -} - -variable "gcs_bucket" { - type = string - description = "GCS bucket name" - default = "" -} - -variable "gcs_force_destroy" { - type = bool - description = "Allow force deletion of bucket objects" - default = false -} - -variable "gcs_uniform_bucket_level_access" { - type = bool - description = "Enable uniform bucket-level access" - default = true -} - -variable "cloud_sql_instance_name" { - type = string - description = "Cloud SQL instance name" - default = "" -} - -variable "cloud_sql_database_version" { - type = string - description = "Cloud SQL database version" - default = "POSTGRES_17" -} - -variable "cloud_sql_tier" { - type = string - description = "Cloud SQL instance tier" - default = "db-g1-small" -} - -variable "cloud_sql_disk_size" { - type = number - description = "Cloud SQL disk size in GB" - default = 100 -} - -variable "cloud_sql_disk_type" { - type = string - description = "Cloud SQL disk type" - default = "PD_SSD" -} - -variable "cloud_sql_availability_type" { - type = string - description = "Cloud SQL availability type" - default = "REGIONAL" -} - -variable "cloud_sql_private_ip_prefix_length" { - type = number - description = "Prefix length for private services range" - default = 16 -} - -variable "cloud_sql_enable_private_service_connection" { - type = bool - default = true -} - -variable "cloud_sql_enable_public_ip" { - type = bool - default = false -} - -variable "cloud_sql_db_name" { - type = string - default = "" -} - -variable "cloud_sql_backup_enabled" { - type = bool - default = true -} - -variable "cloud_sql_deletion_protection" { - type = bool - default = true -} - -variable "redis_instance_name" { - type = string - description = "Memorystore instance name" - default = "" -} - -variable "redis_tier" { - type = string - default = "STANDARD_HA" -} - -variable "redis_memory_size_gb" { - type = number - default = 4 -} - -variable "redis_transit_encryption_mode" { - type = string - default = "DISABLED" -} - -variable "filestore_instance_name" { - type = string - default = "" -} - -variable "filestore_tier" { - type = string - default = "STANDARD" -} - -variable "filestore_capacity_gb" { - type = number - default = 1024 -} - -variable "filestore_file_share_name" { - type = string - default = "share1" -} - -variable "filestore_reserved_ip_range" { - type = string - default = null -} - -# Cloud Run application variables -variable "app_service_name" { - type = string - description = "Cloud Run service name" - default = "" -} - -variable "app_image" { - type = string - description = "Container image" - default = "" -} - -variable "app_env" { - type = map(string) - description = "Environment variables for Cloud Run" - default = {} -} - -variable "app_cpu" { - type = string - default = "1" -} - -variable "app_memory" { - type = string - default = "512Mi" -} - -variable "app_min_instances" { - type = number - default = 0 -} - -variable "app_max_instances" { - type = number - default = 10 -} - -variable "app_allow_unauth" { - type = bool - default = true -} - -variable "storage_key" { - type = string - sensitive = true - default = "" -} - -variable "storage_secret_key" { - type = string - sensitive = true - default = "" -} - -variable "storage_bucket" { - type = string - default = "" -} - -variable "db_username" { - type = string - sensitive = true - default = "" -} - -variable "db_password" { - type = string - sensitive = true - default = "" -} - -variable "db_schema" { - type = string - default = "" -} - -variable "rails_master_key" { - type = string - sensitive = true - default = "" -} - -variable "rails_env" { - type = string - default = "" -} - -variable "ui_env" { - type = string - default = "" -} - -# Cloud Run UI variables -variable "ui_service_name" { - type = string - description = "Cloud Run service name for UI" - default = "" -} - -variable "ui_image" { - type = string - description = "Container image for UI" - default = "" -} - -variable "ui_env_vars" { - type = map(string) - description = "Environment variables for UI Cloud Run" - default = {} -} - -variable "ui_cpu" { - type = string - default = "1" -} - -variable "ui_memory" { - type = string - default = "512Mi" -} - -variable "ui_min_instances" { - type = number - default = 0 -} - -variable "ui_max_instances" { - type = number - default = 10 -} - -variable "ui_allow_unauth" { - type = bool - default = true -} - -variable "app_suffix" { - type = string - default = "" -} - -variable "iam_service_accounts" { - type = map(object({ - display_name = optional(string) - description = optional(string) - roles = optional(list(string), []) - wi_bindings = optional(list(object({ - namespace = string - k8s_service_account_name = string - })), []) - })) - default = {} -} +# NOTE: cleaned up variables, removed k8s/ingress legacy blocks + +variable "project_id" { + type = string +} + +variable "region" { + type = string + default = "us-central1" +} + +variable "zone" { + type = string + description = "GCP zone for zonal resources (e.g. Filestore)." + default = "" +} + +variable "name_prefix" { + type = string + default = "mega-prod" +} + +variable "base_domain" { + type = string + default = "" +} + +variable "enable_build_env" { + type = bool + description = "(deprecated) Was used for GKE build env. Default to false after migration to Cloud Run." + default = false +} + +variable "enable_gcs" { + type = bool + default = true +} + +variable "enable_cloud_sql" { + type = bool + default = true +} + +variable "enable_redis" { + type = bool + default = true +} + +variable "enable_filestore" { + type = bool + default = true +} + +variable "enable_apps" { + type = bool + default = true +} + +variable "enable_logging" { + type = bool + default = true +} + +variable "enable_monitoring" { + type = bool + default = true +} + +variable "enable_alerts" { + type = bool + default = true +} + +variable "alert_notification_channels" { + type = list(string) + default = [] + description = "List of notification channel IDs for alerts" +} + +variable "log_sink_name" { + type = string + default = "" +} + +variable "log_sink_destination" { + type = string + default = "" +} + +variable "network_name" { + type = string + default = "mega-prod-net" +} + +variable "subnet_name" { + type = string + default = "mega-prod-subnet" +} + +variable "subnet_cidr" { + type = string + default = "10.40.0.0/16" +} + +variable "pods_secondary_range" { + type = string + default = "10.41.0.0/16" +} + +variable "services_secondary_range" { + type = string + default = "10.42.0.0/16" +} + +# Private networking for Cloud SQL / Redis (Cloud Run -> VPC Connector) +variable "enable_private_networking" { + type = bool + default = true +} + +variable "vpc_connector_name" { + type = string + default = "" +} + +variable "vpc_connector_cidr" { + type = string + default = null +} + +variable "cloud_run_vpc_egress" { + type = string + default = "private-ranges-only" +} + +variable "artifact_registry_location" { + type = string + default = "us-central1" +} + +variable "artifact_registry_repo" { + type = string + description = "Artifact Registry repository name" + default = "mega-prod" +} + +variable "gcs_bucket" { + type = string + description = "GCS bucket name" + default = "" +} + +variable "gcs_force_destroy" { + type = bool + description = "Allow force deletion of bucket objects" + default = false +} + +variable "gcs_uniform_bucket_level_access" { + type = bool + description = "Enable uniform bucket-level access" + default = true +} + +variable "cloud_sql_instance_name" { + type = string + description = "Cloud SQL instance name" + default = "" +} + +variable "cloud_sql_database_version" { + type = string + description = "Cloud SQL database version" + default = "POSTGRES_17" +} + +variable "cloud_sql_tier" { + type = string + description = "Cloud SQL instance tier" + default = "db-g1-small" +} + +variable "cloud_sql_disk_size" { + type = number + description = "Cloud SQL disk size in GB" + default = 100 +} + +variable "cloud_sql_disk_type" { + type = string + description = "Cloud SQL disk type" + default = "PD_SSD" +} + +variable "cloud_sql_availability_type" { + type = string + description = "Cloud SQL availability type" + default = "REGIONAL" +} + +variable "cloud_sql_private_ip_prefix_length" { + type = number + description = "Prefix length for private services range" + default = 16 +} + +variable "cloud_sql_enable_private_service_connection" { + type = bool + default = true +} + +variable "cloud_sql_enable_public_ip" { + type = bool + default = false +} + +variable "cloud_sql_db_name" { + type = string + default = "" +} + +variable "cloud_sql_backup_enabled" { + type = bool + default = true +} + +variable "cloud_sql_deletion_protection" { + type = bool + default = true +} + +variable "redis_instance_name" { + type = string + description = "Memorystore instance name" + default = "" +} + +variable "redis_tier" { + type = string + default = "STANDARD_HA" +} + +variable "redis_memory_size_gb" { + type = number + default = 4 +} + +variable "redis_transit_encryption_mode" { + type = string + default = "DISABLED" +} + +variable "filestore_instance_name" { + type = string + default = "" +} + +variable "filestore_tier" { + type = string + default = "STANDARD" +} + +variable "filestore_capacity_gb" { + type = number + default = 1024 +} + +variable "filestore_file_share_name" { + type = string + default = "share1" +} + +variable "filestore_reserved_ip_range" { + type = string + default = null +} + +# Cloud Run application variables +variable "app_service_name" { + type = string + description = "Cloud Run service name" + default = "" +} + +variable "app_image" { + type = string + description = "Container image" + default = "" +} + +variable "app_env" { + type = map(string) + description = "Environment variables for Cloud Run" + default = {} +} + +variable "app_cpu" { + type = string + default = "1" +} + +variable "app_memory" { + type = string + default = "512Mi" +} + +variable "app_min_instances" { + type = number + default = 0 +} + +variable "app_max_instances" { + type = number + default = 10 +} + +variable "app_allow_unauth" { + type = bool + default = true +} + +variable "storage_key" { + type = string + sensitive = true + default = "" +} + +variable "storage_secret_key" { + type = string + sensitive = true + default = "" +} + +variable "storage_bucket" { + type = string + default = "" +} + +variable "db_username" { + type = string + sensitive = true + default = "" +} + +variable "db_password" { + type = string + sensitive = true + default = "" +} + +variable "db_schema" { + type = string + default = "" +} + +variable "rails_master_key" { + type = string + sensitive = true + default = "" +} + +variable "rails_env" { + type = string + default = "" +} + +variable "ui_env" { + type = string + default = "" +} + +# Cloud Run UI variables +variable "ui_service_name" { + type = string + description = "Cloud Run service name for UI" + default = "" +} + +variable "ui_image" { + type = string + description = "Container image for UI" + default = "" +} + +variable "ui_env_vars" { + type = map(string) + description = "Environment variables for UI Cloud Run" + default = {} +} + +variable "ui_cpu" { + type = string + default = "1" +} + +variable "ui_memory" { + type = string + default = "512Mi" +} + +variable "ui_min_instances" { + type = number + default = 0 +} + +variable "ui_max_instances" { + type = number + default = 10 +} + +variable "ui_allow_unauth" { + type = bool + default = true +} + +# HTTPS Load Balancer & Routing Strategy (Milestone A) +variable "enable_lb" { + type = bool + description = "Whether to enable Global HTTPS Load Balancer" + default = false +} + +variable "lb_domain" { + type = string + description = "The FQDN for the load balancer (e.g., buck2hub.com)" + default = "buck2hub.com" +} + +variable "lb_api_path_prefixes" { + type = list(string) + description = "URL path prefixes to be routed to the backend service" + default = ["/api/v1", "/info/lfs"] +} + +variable "app_suffix" { + type = string + default = "" +} + +variable "iam_service_accounts" { + type = map(object({ + display_name = optional(string) + description = optional(string) + roles = optional(list(string), []) + wi_bindings = optional(list(object({ + namespace = string + k8s_service_account_name = string + })), []) + })) + default = {} +} diff --git a/modules/gcp/load_balancer/main.tf b/modules/gcp/load_balancer/main.tf new file mode 100755 index 0000000..89bd910 --- /dev/null +++ b/modules/gcp/load_balancer/main.tf @@ -0,0 +1,202 @@ +variable "project_id" { + type = string +} + +variable "region" { + type = string +} + +variable "name_prefix" { + type = string +} + +variable "backend_service_name" { + type = string + description = "Cloud Run service name for backend (mono)" +} + +variable "ui_service_name" { + type = string + description = "Cloud Run service name for UI (Next.js)" + default = "" +} + +variable "lb_domain" { + type = string + description = "Domain name for the load balancer" +} + +variable "api_path_prefixes" { + type = list(string) + description = "Path prefixes to route to backend" + default = ["/api/v1", "/info/lfs"] +} + +# --- 1. Serverless NEGs (Milestone B) --- + +resource "google_compute_region_network_endpoint_group" "backend" { + project = var.project_id + region = var.region + name = "${var.name_prefix}-backend-neg" + network_endpoint_type = "SERVERLESS" + + cloud_run { + service = var.backend_service_name + } +} + +resource "google_compute_region_network_endpoint_group" "ui" { + count = var.ui_service_name != "" ? 1 : 0 + project = var.project_id + region = var.region + name = "${var.name_prefix}-ui-neg" + network_endpoint_type = "SERVERLESS" + + cloud_run { + service = var.ui_service_name + } +} + +# --- 2. Backend Services (Milestone B) --- + +resource "google_compute_backend_service" "backend" { + project = var.project_id + name = "${var.name_prefix}-backend-bs" + protocol = "HTTP" + load_balancing_scheme = "EXTERNAL_MANAGED" + + backend { + group = google_compute_region_network_endpoint_group.backend.id + } +} + +resource "google_compute_backend_service" "ui" { + count = var.ui_service_name != "" ? 1 : 0 + project = var.project_id + name = "${var.name_prefix}-ui-bs" + protocol = "HTTP" + load_balancing_scheme = "EXTERNAL_MANAGED" + + backend { + group = google_compute_region_network_endpoint_group.ui[0].id + } +} + +# --- 3. URL Map (Milestone C) --- + +resource "google_compute_url_map" "this" { + project = var.project_id + name = "${var.name_prefix}-urlmap" + + default_service = var.ui_service_name != "" ? google_compute_backend_service.ui[0].self_link : google_compute_backend_service.backend.self_link + + host_rule { + hosts = [var.lb_domain] + path_matcher = "pm-default" + } + + path_matcher { + name = "pm-default" + default_service = var.ui_service_name != "" ? google_compute_backend_service.ui[0].self_link : google_compute_backend_service.backend.self_link + + dynamic "path_rule" { + for_each = var.api_path_prefixes + content { + paths = ["${path_rule.value}/*"] + service = google_compute_backend_service.backend.self_link + } + } + } +} + +# --- 4. Load Balancer Entry (Milestone D) --- + +resource "google_compute_global_address" "this" { + project = var.project_id + name = "${var.name_prefix}-lb-ip" +} + +resource "google_compute_target_https_proxy" "this" { + project = var.project_id + name = "${var.name_prefix}-https-proxy" + url_map = google_compute_url_map.this.id + certificate_map = "//certificatemanager.googleapis.com/${google_certificate_manager_certificate_map.this.id}" +} + +resource "google_compute_global_forwarding_rule" "https" { + project = var.project_id + name = "${var.name_prefix}-https-fr" + target = google_compute_target_https_proxy.this.id + port_range = "443" + ip_address = google_compute_global_address.this.address + load_balancing_scheme = "EXTERNAL_MANAGED" +} + +# --- 5. Certificate Manager (Milestone E) --- + +resource "google_certificate_manager_dns_authorization" "this" { + project = var.project_id + name = "${var.name_prefix}-dns-auth" + domain = var.lb_domain + description = "DNS authorization for ${var.lb_domain}" +} + +resource "google_certificate_manager_certificate" "this" { + project = var.project_id + name = "${var.name_prefix}-cert" + description = "Google-managed cert for ${var.lb_domain}" + scope = "DEFAULT" + managed { + domains = [var.lb_domain] + dns_authorizations = [ + google_certificate_manager_dns_authorization.this.id + ] + } +} + +resource "google_certificate_manager_certificate_map" "this" { + project = var.project_id + name = "${var.name_prefix}-cert-map" + description = "Certificate map for ${var.lb_domain}" +} + +resource "google_certificate_manager_certificate_map_entry" "this" { + project = var.project_id + name = "${var.name_prefix}-cert-map-entry" + map = google_certificate_manager_certificate_map.this.name + certificates = [google_certificate_manager_certificate.this.id] + hostname = var.lb_domain +} + +# --- Outputs --- + +output "lb_ip" { + value = google_compute_global_address.this.address +} + +output "dns_authorization_record_name" { + description = "DNS CNAME record name for cert verification" + value = google_certificate_manager_dns_authorization.this.dns_resource_record[0].name +} + +output "dns_authorization_record_value" { + description = "DNS CNAME record value for cert verification" + value = google_certificate_manager_dns_authorization.this.dns_resource_record[0].data +} + +output "dns_authorization_record_type" { + description = "DNS record type for cert verification" + value = google_certificate_manager_dns_authorization.this.dns_resource_record[0].type +} + +output "backend_backend_service_self_link" { + value = google_compute_backend_service.backend.self_link +} + +output "ui_backend_service_self_link" { + value = var.ui_service_name != "" ? google_compute_backend_service.ui[0].self_link : null +} + +output "url_map_self_link" { + value = google_compute_url_map.this.self_link +}