mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-05-13 09:35:30 -04:00
Move backend from VM to Cloud Run and remove Load Balancer
Both to save lots of $$$
This commit is contained in:
13
.github/workflows/cd-api.yml
vendored
13
.github/workflows/cd-api.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: API Release
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
branches: [ main, master ]
|
||||
paths:
|
||||
- 'backend/api/package.json'
|
||||
- '.github/workflows/cd-api.yml'
|
||||
@@ -70,17 +70,6 @@ jobs:
|
||||
- name: Configure Docker for Artifact Registry
|
||||
run: gcloud auth configure-docker us-west1-docker.pkg.dev --quiet
|
||||
|
||||
- name: Install Tofu (Terraform)
|
||||
run: |
|
||||
LATEST=https://github.com/opentofu/opentofu/releases/download/v1.10.5/tofu_1.10.5_linux_amd64.zip
|
||||
curl -LO "$LATEST"
|
||||
unzip -o tofu_*_linux_amd64.zip
|
||||
sudo mv tofu /usr/local/bin/
|
||||
rm tofu_*_linux_amd64.zip
|
||||
echo "OpenTofu version: $(tofu version)"
|
||||
cd backend/api
|
||||
tofu init
|
||||
|
||||
- name: Run deploy script
|
||||
run: |
|
||||
chmod +x backend/api/deploy-api.sh
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM node:20-alpine
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install PM2 globally
|
||||
RUN yarn global add pm2
|
||||
# RUN yarn global add pm2
|
||||
|
||||
# Fet dependencies in for efficient docker layering
|
||||
COPY dist/package.json dist/yarn.lock ./
|
||||
@@ -21,13 +21,15 @@ RUN npm list || true
|
||||
COPY dist ./
|
||||
|
||||
# Copy the PM2 ecosystem configuration
|
||||
COPY ecosystem.config.js ./
|
||||
# COPY ecosystem.config.js ./
|
||||
|
||||
ENV PORT=80
|
||||
EXPOSE 80/tcp
|
||||
#ENV PORT=80
|
||||
#EXPOSE 80/tcp
|
||||
# EXPOSE 8090/tcp
|
||||
# EXPOSE 8091/tcp
|
||||
# EXPOSE 8092/tcp
|
||||
|
||||
# Use PM2 to run the application with the ecosystem config
|
||||
CMD ["pm2-runtime", "ecosystem.config.js"]
|
||||
# Use PM2 to run the application with the ecosystem config (was only for VM, not cloud run)
|
||||
#CMD ["pm2-runtime", "ecosystem.config.js"]
|
||||
|
||||
CMD ["node", "-r", "tsconfig-paths/register", "backend/api/lib/serve.js"]
|
||||
@@ -1,14 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# steps to deploy new version to GCP:
|
||||
# 1. build new docker image & upload to Google
|
||||
# 2. create a new GCP instance template with the new docker image
|
||||
# 3. tell the GCP 'backend service' for the API to update to the new template
|
||||
# 4. a. GCP creates a new instance with the new template
|
||||
# b. wait for the new instance to be healthy (serving TCP connections)
|
||||
# c. route new connections to the new instance
|
||||
# d. delete the old instance
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
@@ -45,28 +35,21 @@ TIMESTAMP=$(date +"%s")
|
||||
IMAGE_TAG="${TIMESTAMP}-${GIT_REVISION}"
|
||||
IMAGE_URL="${REGION}-docker.pkg.dev/${PROJECT}/builds/${SERVICE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
echo "🚀 Deploying ${SERVICE_NAME} to ${ENV} ($(date "+%Y-%m-%d %I:%M:%S %p"))"
|
||||
echo "🚀 Building & Pushing Image..."
|
||||
yarn build
|
||||
|
||||
gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin us-west1-docker.pkg.dev
|
||||
docker build . --tag ${IMAGE_URL} --platform linux/amd64 --progress=plain
|
||||
echo "docker push ${IMAGE_URL}"
|
||||
gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin ${REGION}-docker.pkg.dev
|
||||
gcloud auth configure-docker ${REGION}-docker.pkg.dev --quiet
|
||||
docker build . --tag ${IMAGE_URL} --platform linux/amd64
|
||||
docker push ${IMAGE_URL}
|
||||
|
||||
export TF_VAR_image_url=$IMAGE_URL
|
||||
export TF_VAR_env=$ENV
|
||||
tofu apply -auto-approve
|
||||
# Update Cloud Run (The fast way)
|
||||
# This keeps all the Terraform-defined settings (env vars, memory, etc.)
|
||||
# but simply swaps the container image.
|
||||
gcloud run deploy ${SERVICE_NAME} \
|
||||
--image ${IMAGE_URL} \
|
||||
--region ${REGION} \
|
||||
--platform managed \
|
||||
--quiet
|
||||
|
||||
#INSTANCE_NAME=$(gcloud compute instances list \
|
||||
# --filter="zone:(us-west1-c)" \
|
||||
# --sort-by="~creationTimestamp" \
|
||||
# --format="value(name)" \
|
||||
# --limit=1)
|
||||
#SERVICE_ACCOUNT_EMAIL=$(gcloud compute instances describe ${INSTANCE_NAME} \
|
||||
# --zone us-west1-c \
|
||||
# --format="value(serviceAccounts.email)")
|
||||
#gcloud projects add-iam-policy-binding ${PROJECT} \
|
||||
# --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \
|
||||
# --role="roles/artifactregistry.reader"
|
||||
|
||||
echo "✅ Deployment complete! Image: ${IMAGE_URL}"
|
||||
echo "Custom Domain: https://api.compassmeet.com"
|
||||
echo "✅ Code updated on Cloud Run!"
|
||||
|
||||
54
backend/api/deploy-init-api.sh
Executable file
54
backend/api/deploy-init-api.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ../../.env
|
||||
|
||||
ENV=${1:-prod}
|
||||
|
||||
# Config
|
||||
REGION="us-west1"
|
||||
ZONE="us-west1-b"
|
||||
|
||||
PROJECT="compass-130ba"
|
||||
SERVICE_NAME="api"
|
||||
|
||||
GIT_REVISION=$(git rev-parse --short HEAD)
|
||||
GIT_COMMIT_DATE=$(git log -1 --format=%ci)
|
||||
GIT_COMMIT_AUTHOR=$(git log -1 --format='%an')
|
||||
GIT_COMMIT_MESSAGE=$(git log -1 --format='%s')
|
||||
echo "Git commit message: ${GIT_COMMIT_MESSAGE}"
|
||||
|
||||
cat > metadata.json << EOF
|
||||
{
|
||||
"git": {
|
||||
"revision": "${GIT_REVISION}",
|
||||
"commitDate": "${GIT_COMMIT_DATE}",
|
||||
"author": "${GIT_COMMIT_AUTHOR}",
|
||||
"message": "${GIT_COMMIT_MESSAGE}"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
TIMESTAMP=$(date +"%s")
|
||||
IMAGE_TAG="${TIMESTAMP}-${GIT_REVISION}"
|
||||
IMAGE_URL="${REGION}-docker.pkg.dev/${PROJECT}/builds/${SERVICE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
echo "🚀 Building & Pushing Image..."
|
||||
yarn build
|
||||
gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin ${REGION}-docker.pkg.dev
|
||||
docker build . --tag ${IMAGE_URL} --platform linux/amd64
|
||||
docker push ${IMAGE_URL}
|
||||
|
||||
echo "Infrastructure Update..."
|
||||
export TF_VAR_image_url=$IMAGE_URL
|
||||
export TF_VAR_env=$ENV
|
||||
tofu apply -auto-approve
|
||||
|
||||
# Get the new URL just in case
|
||||
SERVICE_URL=$(gcloud run services describe ${SERVICE_NAME} --platform managed --region ${REGION} --format 'value(status.url)')
|
||||
|
||||
echo "✅ Deployed to Cloud Run!"
|
||||
echo "Service URL: ${SERVICE_URL}"
|
||||
echo "Custom Domain: https://api.compassmeet.com"
|
||||
@@ -1,7 +1,7 @@
|
||||
# Variables
|
||||
variable "image_url" {
|
||||
description = "Docker image URL"
|
||||
type = string
|
||||
default = "us-west1-docker.pkg.dev/compass-130ba/builds/api:latest"
|
||||
}
|
||||
|
||||
variable "env" {
|
||||
@@ -10,324 +10,97 @@ variable "env" {
|
||||
default = "prod"
|
||||
}
|
||||
|
||||
# 2. Local Constants
|
||||
locals {
|
||||
project = "compass-130ba"
|
||||
region = "us-west1"
|
||||
zone = "us-west1-b"
|
||||
service_name = "api"
|
||||
machine_type = "e2-small"
|
||||
}
|
||||
|
||||
# 3. Provider & Backend
|
||||
terraform {
|
||||
backend "gcs" {
|
||||
bucket = "compass-130ba-terraform-state"
|
||||
prefix = "api"
|
||||
prefix = "api-cloudrun" # Changed prefix so it doesn't collide with old state
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = local.project
|
||||
region = local.region
|
||||
zone = local.zone
|
||||
}
|
||||
|
||||
# Firebase Storage Buckets
|
||||
# Note you still have to deploy the rules: `firebase deploy --only storage`
|
||||
resource "google_storage_bucket" "public_storage" {
|
||||
# /!\ That bucket is different from the one in firebase (compass-130ba.firebasestorage.app)
|
||||
# as it errors when trying to do so:
|
||||
# Error: googleapi: Error 403: Another user owns the domain compass-130ba.firebasestorage.app or a parent domain. You can either verify domain ownership at https://search.google.com/search-console/welcome?new_domain_name=compass-130ba.firebasestorage.app or find the current owner and ask that person to create the bucket for you, forbidden
|
||||
# To be fixed later if they must be the same bucket (shared resources)
|
||||
name = "compass-130ba"
|
||||
location = "US"
|
||||
force_destroy = false
|
||||
# The Cloud Run Service
|
||||
resource "google_cloud_run_v2_service" "api" {
|
||||
name = local.service_name
|
||||
location = local.region
|
||||
ingress = "INGRESS_TRAFFIC_ALL"
|
||||
|
||||
uniform_bucket_level_access = true
|
||||
template {
|
||||
startup_cpu_boost = true
|
||||
|
||||
cors {
|
||||
origin = ["*"]
|
||||
method = ["GET", "HEAD", "PUT", "POST", "DELETE"]
|
||||
response_header = ["*"]
|
||||
max_age_seconds = 3600
|
||||
}
|
||||
}
|
||||
scaling {
|
||||
min_instance_count = 0 # This enables scaling to zero (saves money!)
|
||||
max_instance_count = 10
|
||||
}
|
||||
|
||||
containers {
|
||||
image = var.image_url
|
||||
|
||||
# static IPs
|
||||
resource "google_compute_global_address" "api_lb_ip" {
|
||||
name = "api-lb-ip-2"
|
||||
address_type = "EXTERNAL"
|
||||
}
|
||||
resources {
|
||||
limits = {
|
||||
cpu = "1" # 1 vCPU is standard, increase to "2" if heavy traffic
|
||||
memory = "1Gi"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_managed_ssl_certificate" "api_cert" {
|
||||
name = "api-lb-cert-1"
|
||||
ports {
|
||||
container_port = 8080
|
||||
}
|
||||
|
||||
managed {
|
||||
domains = ["api.compassmeet.com"]
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "NEXT_PUBLIC_FIREBASE_ENV"
|
||||
value = upper(var.env)
|
||||
}
|
||||
env {
|
||||
name = "GOOGLE_CLOUD_PROJECT"
|
||||
value = local.project
|
||||
}
|
||||
|
||||
# Instance template with your Docker container
|
||||
resource "google_compute_instance_template" "api_template" {
|
||||
name_prefix = "${local.service_name}-"
|
||||
machine_type = local.machine_type
|
||||
|
||||
tags = ["lb-health-check"]
|
||||
|
||||
disk {
|
||||
source_image = "cos-cloud/cos-stable" # Container-Optimized OS
|
||||
auto_delete = true
|
||||
boot = true
|
||||
}
|
||||
|
||||
network_interface {
|
||||
network = "default"
|
||||
subnetwork = "default"
|
||||
access_config {
|
||||
network_tier = "PREMIUM"
|
||||
# Optional: CPU Boost speeds up cold starts significantly
|
||||
startup_probe {
|
||||
initial_delay_seconds = 0
|
||||
timeout_seconds = 1
|
||||
period_seconds = 3
|
||||
failure_threshold = 3
|
||||
tcp_socket {
|
||||
port = 8080
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service_account {
|
||||
scopes = ["cloud-platform"]
|
||||
}
|
||||
|
||||
metadata = {
|
||||
gce-container-declaration = <<EOF
|
||||
spec:
|
||||
containers:
|
||||
- image: '${var.image_url}'
|
||||
env:
|
||||
- name: NEXT_PUBLIC_FIREBASE_ENV
|
||||
value: ${upper(var.env)}
|
||||
- name: GOOGLE_CLOUD_PROJECT
|
||||
value: ${local.project}
|
||||
ports:
|
||||
- containerPort: 80
|
||||
EOF
|
||||
google-logging-enabled = "true"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
# Managed instance group (for 1 VM)
|
||||
resource "google_compute_region_instance_group_manager" "api_group" {
|
||||
name = "${local.service_name}-group"
|
||||
base_instance_name = "${local.service_name}-group"
|
||||
region = local.region
|
||||
target_size = 1
|
||||
|
||||
version {
|
||||
instance_template = google_compute_instance_template.api_template.id
|
||||
name = "primary"
|
||||
}
|
||||
|
||||
update_policy {
|
||||
type = "PROACTIVE"
|
||||
minimal_action = "REPLACE"
|
||||
max_unavailable_fixed = 0
|
||||
max_surge_fixed = 3
|
||||
}
|
||||
|
||||
named_port {
|
||||
name = "http"
|
||||
port = 80
|
||||
}
|
||||
|
||||
auto_healing_policies {
|
||||
health_check = google_compute_health_check.api_health_check.id
|
||||
initial_delay_sec = 300
|
||||
}
|
||||
# Allow public (unauthenticated) access to the API
|
||||
resource "google_cloud_run_v2_service_iam_member" "public_access" {
|
||||
location = google_cloud_run_v2_service.api.location
|
||||
name = google_cloud_run_v2_service.api.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
resource "google_compute_health_check" "api_health_check" {
|
||||
name = "${local.service_name}-health-check"
|
||||
check_interval_sec = 5
|
||||
timeout_sec = 5
|
||||
healthy_threshold = 2
|
||||
unhealthy_threshold = 10
|
||||
# Free Domain Mapping (Replaces the Load Balancer)
|
||||
# Note: Check if your region supports 'google_cloud_run_domain_mapping'
|
||||
# Otherwise, use 'google_cloud_run_v2_domain_mapping'
|
||||
resource "google_cloud_run_domain_mapping" "api_domain" {
|
||||
location = local.region
|
||||
name = "api.compassmeet.com"
|
||||
|
||||
tcp_health_check {
|
||||
port = "80"
|
||||
}
|
||||
}
|
||||
|
||||
# Backend service
|
||||
resource "google_compute_backend_service" "api_backend" {
|
||||
name = "${local.service_name}-backend"
|
||||
protocol = "HTTP"
|
||||
port_name = "http"
|
||||
timeout_sec = 30
|
||||
|
||||
health_checks = [google_compute_health_check.api_health_check.id]
|
||||
|
||||
backend {
|
||||
group = google_compute_region_instance_group_manager.api_group.instance_group
|
||||
metadata {
|
||||
namespace = local.project
|
||||
}
|
||||
|
||||
log_config {
|
||||
enable = true
|
||||
spec {
|
||||
route_name = google_cloud_run_v2_service.api.name
|
||||
}
|
||||
}
|
||||
|
||||
# URL map
|
||||
resource "google_compute_url_map" "api_url_map" {
|
||||
name = "${local.service_name}-url-map"
|
||||
default_service = google_compute_backend_service.api_backend.self_link
|
||||
|
||||
host_rule {
|
||||
hosts = ["*"]
|
||||
path_matcher = "allpaths"
|
||||
}
|
||||
|
||||
path_matcher {
|
||||
name = "allpaths"
|
||||
default_service = google_compute_backend_service.api_backend.self_link
|
||||
#
|
||||
# # Priority 0: passthrough /v0/* requests
|
||||
# route_rules {
|
||||
# priority = 1
|
||||
# match_rules {
|
||||
# prefix_match = "/v0"
|
||||
# }
|
||||
# service = google_compute_backend_service.api_backend.self_link
|
||||
# }
|
||||
#
|
||||
# # Priority 1: rewrite everything else to /v0
|
||||
# route_rules {
|
||||
# priority = 2
|
||||
# match_rules {
|
||||
# prefix_match = "/"
|
||||
# }
|
||||
# route_action {
|
||||
# url_rewrite { # This may break websockets (the Upgrade and Connection headers must pass through untouched).
|
||||
# path_prefix_rewrite = "/v0/"
|
||||
# }
|
||||
# }
|
||||
# service = google_compute_backend_service.api_backend.self_link
|
||||
# }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# HTTPS proxy
|
||||
resource "google_compute_target_https_proxy" "api_https_proxy" {
|
||||
name = "${local.service_name}-https-proxy"
|
||||
url_map = google_compute_url_map.api_url_map.id
|
||||
ssl_certificates = [google_compute_managed_ssl_certificate.api_cert.id]
|
||||
}
|
||||
|
||||
# Global forwarding rule (load balancer frontend)
|
||||
resource "google_compute_global_forwarding_rule" "api_https_forwarding_rule" {
|
||||
name = "${local.service_name}-https-forwarding-rule-2"
|
||||
target = google_compute_target_https_proxy.api_https_proxy.id
|
||||
port_range = "443"
|
||||
ip_address = google_compute_global_address.api_lb_ip.id
|
||||
}
|
||||
|
||||
# HTTP-to-HTTPS redirect
|
||||
resource "google_compute_url_map" "api_http_redirect" {
|
||||
name = "${local.service_name}-http-redirect"
|
||||
|
||||
default_url_redirect {
|
||||
https_redirect = true
|
||||
redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
|
||||
strip_query = false
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_target_http_proxy" "api_http_proxy" {
|
||||
name = "${local.service_name}-http-proxy"
|
||||
url_map = google_compute_url_map.api_http_redirect.id
|
||||
}
|
||||
|
||||
resource "google_compute_global_forwarding_rule" "api_http_forwarding_rule" {
|
||||
name = "${local.service_name}-http-forwarding-rule"
|
||||
target = google_compute_target_http_proxy.api_http_proxy.id
|
||||
port_range = "80"
|
||||
ip_address = google_compute_global_address.api_lb_ip.id
|
||||
}
|
||||
|
||||
|
||||
# Firewalls
|
||||
|
||||
resource "google_compute_firewall" "allow_health_check" {
|
||||
name = "allow-health-check-${local.service_name}"
|
||||
network = "default"
|
||||
|
||||
allow {
|
||||
protocol = "tcp"
|
||||
ports = ["80"]
|
||||
}
|
||||
|
||||
source_ranges = ["130.211.0.0/22", "35.191.0.0/16"]
|
||||
target_tags = ["lb-health-check"]
|
||||
}
|
||||
|
||||
resource "google_compute_firewall" "default_allow_https" {
|
||||
name = "default-allow-http"
|
||||
network = "default"
|
||||
priority = 1000
|
||||
direction = "INGRESS"
|
||||
|
||||
allow {
|
||||
protocol = "tcp"
|
||||
ports = ["80", "443"] # ["443", "8090-8099"]
|
||||
}
|
||||
|
||||
source_ranges = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
# resource "google_compute_firewall" "default_allow_ssh" {
|
||||
# name = "default-allow-ssh"
|
||||
# network = "default"
|
||||
# priority = 65534
|
||||
# direction = "INGRESS"
|
||||
#
|
||||
# allow {
|
||||
# protocol = "tcp"
|
||||
# ports = ["22"]
|
||||
# }
|
||||
#
|
||||
# source_ranges = ["0.0.0.0/0"]
|
||||
# }
|
||||
#
|
||||
# resource "google_compute_firewall" "default_allow_internal" {
|
||||
# name = "default-allow-internal"
|
||||
# network = "default"
|
||||
# priority = 65534
|
||||
# direction = "INGRESS"
|
||||
#
|
||||
# allow {
|
||||
# protocol = "tcp"
|
||||
# ports = ["0-65535"]
|
||||
# }
|
||||
#
|
||||
# allow {
|
||||
# protocol = "udp"
|
||||
# ports = ["0-65535"]
|
||||
# }
|
||||
#
|
||||
# allow {
|
||||
# protocol = "icmp"
|
||||
# }
|
||||
#
|
||||
# source_ranges = ["10.128.0.0/9"]
|
||||
# }
|
||||
#
|
||||
# # Allow ICMP (ping)
|
||||
# resource "google_compute_firewall" "default_allow_icmp" {
|
||||
# name = "default-allow-icmp"
|
||||
# network = "default"
|
||||
# priority = 65534
|
||||
# direction = "INGRESS"
|
||||
#
|
||||
# allow {
|
||||
# protocol = "icmp"
|
||||
# }
|
||||
#
|
||||
# source_ranges = ["0.0.0.0/0"]
|
||||
# }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@compass/api",
|
||||
"version": "1.37.1",
|
||||
"version": "1.38.0",
|
||||
"private": true,
|
||||
"description": "Backend API endpoints",
|
||||
"main": "src/serve.ts",
|
||||
|
||||
Reference in New Issue
Block a user