From 9caac159e163d5d6263d45131943d7477239d325 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Mon, 11 May 2026 16:01:12 +0200 Subject: [PATCH] Move backend from VM to Cloud Run and remove Load Balancer Both to save lots of $$$ --- .github/workflows/cd-api.yml | 13 +- backend/api/Dockerfile | 14 +- backend/api/deploy-api.sh | 45 ++--- backend/api/deploy-init-api.sh | 54 +++++ backend/api/main.tf | 347 ++++++--------------------------- backend/api/package.json | 2 +- 6 files changed, 138 insertions(+), 337 deletions(-) create mode 100755 backend/api/deploy-init-api.sh diff --git a/.github/workflows/cd-api.yml b/.github/workflows/cd-api.yml index f862e776..a5d9d8d0 100644 --- a/.github/workflows/cd-api.yml +++ b/.github/workflows/cd-api.yml @@ -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 diff --git a/backend/api/Dockerfile b/backend/api/Dockerfile index adac631e..36705e8f 100644 --- a/backend/api/Dockerfile +++ b/backend/api/Dockerfile @@ -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"] \ No newline at end of file diff --git a/backend/api/deploy-api.sh b/backend/api/deploy-api.sh index 5001cd3b..3293b7eb 100755 --- a/backend/api/deploy-api.sh +++ b/backend/api/deploy-api.sh @@ -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!" diff --git a/backend/api/deploy-init-api.sh b/backend/api/deploy-init-api.sh new file mode 100755 index 00000000..6278be5b --- /dev/null +++ b/backend/api/deploy-init-api.sh @@ -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" \ No newline at end of file diff --git a/backend/api/main.tf b/backend/api/main.tf index fbae9abe..ae1b4675 100644 --- a/backend/api/main.tf +++ b/backend/api/main.tf @@ -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 = <