Bluewoo HRMS
Micro-Step Build PlanBuilding Blocks

Phase 11: Production Deployment

Deploy HRMS to Google Cloud with Cloud SQL, MongoDB Atlas, Secret Manager, and multi-service CI/CD

Phase 11: Production Deployment

Goal: Deploy the complete HRMS application to Google Cloud Platform with production-grade infrastructure.

AttributeValue
Steps01-20
Estimated Time12-16 hours
DependenciesPhase 10.5 complete (Pre-Launch Features)
Completion GateAll 3 services running on production with custom domains, SSL, and connected databases

Phase Context (READ FIRST)

What This Phase Accomplishes

  • Cloud SQL PostgreSQL: Production database with automatic backups
  • MongoDB Atlas: Vector database for AI service
  • Google Secret Manager: Secure credential storage
  • Redis (Memorystore): Caching and session storage
  • Cloud Storage: Document file storage
  • Multi-Service CI/CD: Automated deployment of web, api, and ai services
  • Custom Domains: Production URLs with SSL certificates

What This Phase Does NOT Include

  • High Availability (HA) setup (future optimization)
  • Multi-region deployment
  • Advanced monitoring/alerting (basic health checks only)
  • Load balancer configuration (Cloud Run handles this)

Prerequisites

Before starting this phase:

  1. GCP Account: With billing enabled
  2. MongoDB Atlas Account: Free tier is fine initially
  3. Domain Access: DNS management for bluewoo.com (Namecheap)
  4. GitHub Repository: With Actions enabled
  5. gcloud CLI: Installed and authenticated
  6. Completed Phases 00-10.5: HRMS app working locally

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        Google Cloud Platform                         │
│                                                                       │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐                │
│  │  Cloud Run  │   │  Cloud Run  │   │  Cloud Run  │                │
│  │    (web)    │   │    (api)    │   │    (ai)     │                │
│  │  Next.js    │   │   NestJS    │   │   Express   │                │
│  └──────┬──────┘   └──────┬──────┘   └──────┬──────┘                │
│         │                 │                 │                        │
│         │         ┌───────┴───────┐         │                        │
│         │         │               │         │                        │
│         │    ┌────▼────┐    ┌─────▼─────┐   │                        │
│         │    │Cloud SQL│    │  Redis    │   │                        │
│         │    │ (Postgres)│   │(Memorystore)│   │                        │
│         │    └─────────┘    └───────────┘   │                        │
│         │                                   │                        │
│    ┌────▼────────────────────────────┐     │                        │
│    │       Cloud Storage (GCS)        │     │                        │
│    │         (documents)              │     │                        │
│    └─────────────────────────────────┘     │                        │
│                                             │                        │
│    ┌────────────────────────────────────────▼──────┐                │
│    │            Secret Manager                      │                │
│    │  (DATABASE_URL, JWT_SECRET, API_KEYS, etc.)   │                │
│    └────────────────────────────────────────────────┘                │
└─────────────────────────────────────────────────────────────────────┘


                    ┌───────────────────────────┐
                    │      MongoDB Atlas        │
                    │   (Vector DB for AI)      │
                    └───────────────────────────┘

Environment Variables Reference

Web Service (Next.js)

# Auth
AUTH_SECRET=<from-secret-manager>
AUTH_URL=https://app.hrms.bluewoo.com
GOOGLE_CLIENT_ID=<from-secret-manager>
GOOGLE_CLIENT_SECRET=<from-secret-manager>

# API
NEXT_PUBLIC_API_URL=https://api.hrms.bluewoo.com

API Service (NestJS)

# Database
DATABASE_URL=<from-secret-manager>

# Auth
JWT_SECRET=<from-secret-manager>
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d

# Redis
REDIS_URL=<from-secret-manager>

# Storage
GCS_BUCKET_NAME=hrms-documents-prod
GOOGLE_APPLICATION_CREDENTIALS=<service-account>

# AI Service
AI_SERVICE_URL=https://ai.hrms.bluewoo.com

AI Service (Express)

# MongoDB
MONGODB_URI=<from-secret-manager>

# OpenAI
OPENAI_API_KEY=<from-secret-manager>

# Backend
HRMS_API_URL=https://api.hrms.bluewoo.com

Step 01: Create GCP Project and Enable APIs

Input

  • GCP account with billing enabled
  • gcloud CLI installed

Constraints

  • Use project ID: bluewoo-hrms
  • Region: europe-west1 (Belgium) for EU data residency
  • DO NOT enable APIs you won't use (costs money)

Task

# Set variables
export PROJECT_ID="bluewoo-hrms"
export REGION="europe-west1"

# Create project (skip if exists)
gcloud projects create $PROJECT_ID --name="Bluewoo HRMS"

# Set as active project
gcloud config set project $PROJECT_ID

# Link billing (get your billing account ID from console)
gcloud billing accounts list
gcloud billing projects link $PROJECT_ID --billing-account=<BILLING_ACCOUNT_ID>

# Enable required APIs
gcloud services enable \
  run.googleapis.com \
  sqladmin.googleapis.com \
  artifactregistry.googleapis.com \
  secretmanager.googleapis.com \
  redis.googleapis.com \
  storage.googleapis.com \
  vpcaccess.googleapis.com \
  servicenetworking.googleapis.com \
  iam.googleapis.com

Gate

# Verify APIs enabled
gcloud services list --enabled | grep -E "(run|sql|artifact|secret|redis|storage|vpc)"
# Should show all 7 APIs enabled

Common Errors

ErrorCauseFix
billing account not foundWrong billing IDRun gcloud billing accounts list to find correct ID
permission deniedNot owner of projectEnsure you're project owner or have roles/owner
quota exceededToo many projectsDelete unused projects or request quota increase

Lock

  • GCP project created
  • APIs enabled

Checkpoint

  • gcloud config get-value project returns bluewoo-hrms
  • All 7 APIs enabled
  • Type "GATE 01 PASSED" to continue

Step 02: Create Artifact Registry

Input

  • Step 01 complete
  • GCP project active

Constraints

  • Single registry for all services
  • Docker format only

Task

# Create Artifact Registry repository
gcloud artifacts repositories create hrms-images \
  --repository-format=docker \
  --location=$REGION \
  --description="Docker images for HRMS services"

# Configure Docker to use Artifact Registry
gcloud auth configure-docker ${REGION}-docker.pkg.dev

Gate

# Verify repository exists
gcloud artifacts repositories list --location=$REGION
# Should show hrms-images repository

Lock

  • hrms-images repository created

Checkpoint

  • Repository shows in gcloud artifacts repositories list
  • Docker configured for Artifact Registry
  • Type "GATE 02 PASSED" to continue

Step 03: Create Cloud SQL PostgreSQL Instance

Input

  • Step 02 complete
  • GCP project with APIs enabled

Constraints

  • PostgreSQL 17
  • Start small (db-f1-micro for staging, upgrade later)
  • Enable automatic backups
  • Private IP for security

Task

# Create Cloud SQL instance (this takes 5-10 minutes)
gcloud sql instances create hrms-db \
  --database-version=POSTGRES_17 \
  --tier=db-f1-micro \
  --region=$REGION \
  --storage-size=10GB \
  --storage-auto-increase \
  --backup-start-time=03:00 \
  --maintenance-window-day=SUN \
  --maintenance-window-hour=04 \
  --availability-type=zonal

# Set root password
gcloud sql users set-password postgres \
  --instance=hrms-db \
  --password=<STRONG_PASSWORD>

Gate

# Verify instance is running
gcloud sql instances describe hrms-db --format="value(state)"
# Should return: RUNNABLE

Common Errors

ErrorCauseFix
instance creation failedQuota exceededCheck SQL instance quota in IAM
timeoutNetwork issuesWait and retry, Cloud SQL takes time

Lock

  • Cloud SQL instance hrms-db created

Checkpoint

  • gcloud sql instances describe hrms-db shows RUNNABLE
  • Root password set
  • Type "GATE 03 PASSED" to continue

Step 04: Create Production Database and User

Input

  • Step 03 complete
  • Cloud SQL instance running

Constraints

  • Separate database for HRMS
  • Dedicated user (not postgres root)
  • Strong password

Task

# Create database
gcloud sql databases create hrms_prod --instance=hrms-db

# Create application user
gcloud sql users create hrms_app \
  --instance=hrms-db \
  --password=<STRONG_APP_PASSWORD>

# Get connection name for later
gcloud sql instances describe hrms-db --format="value(connectionName)"
# Save this: bluewoo-hrms:europe-west1:hrms-db

Gate

# Verify database exists
gcloud sql databases list --instance=hrms-db
# Should show: hrms_prod

# Verify user exists
gcloud sql users list --instance=hrms-db
# Should show: hrms_app and postgres

Lock

  • Database hrms_prod created
  • User hrms_app created

Checkpoint

  • Database hrms_prod exists
  • User hrms_app exists
  • Connection name saved
  • Type "GATE 04 PASSED" to continue

Step 05: Create MongoDB Atlas Cluster

Input

  • Step 04 complete
  • MongoDB Atlas account

Constraints

  • Use Atlas UI (easier than CLI for initial setup)
  • M0 free tier for staging, M10 for production
  • Same region as GCP (europe-west1 = Belgium)

Task

  1. Go to MongoDB Atlas
  2. Create new project: Bluewoo HRMS
  3. Create cluster:
    • Tier: M0 (free) for staging, M10 for production
    • Provider: Google Cloud
    • Region: Belgium (europe-west1)
    • Cluster Name: hrms-ai-staging or hrms-ai-prod
  4. Create database user:
    • Username: hrms_ai_app
    • Password: Generate strong password
    • Roles: readWrite on hrms_ai database
  5. Configure Network Access:
    • Add 0.0.0.0/0 for now (we'll restrict later)
    • Or add GCP Cloud Run egress IPs

Gate

# Test connection (from local machine with mongosh)
mongosh "mongodb+srv://hrms-ai-staging.xxxxx.mongodb.net/" \
  --username hrms_ai_app \
  --password <PASSWORD>

# In mongosh, verify connection
db.runCommand({ ping: 1 })
# Should return: { ok: 1 }

Lock

  • MongoDB Atlas cluster created
  • Database user created
  • Network access configured

Checkpoint

  • Atlas cluster shows "Active" status
  • Can connect with mongosh
  • Connection string saved
  • Type "GATE 05 PASSED" to continue

Step 06: Create Vector Search Index in MongoDB

Input

  • Step 05 complete
  • MongoDB cluster running

Constraints

  • Index name: vector_index
  • Field: embedding
  • Dimensions: 1536 (OpenAI ada-002)

Task

In MongoDB Atlas UI:

  1. Go to DatabaseBrowse Collections
  2. Create database: hrms_ai
  3. Create collection: document_chunks
  4. Go to Search tab → Create Search Index
  5. Use JSON Editor and paste:
{
  "mappings": {
    "dynamic": true,
    "fields": {
      "embedding": {
        "type": "knnVector",
        "dimensions": 1536,
        "similarity": "cosine"
      },
      "tenantId": {
        "type": "string"
      }
    }
  }
}
  1. Name the index: vector_index
  2. Click Create

Gate

// In mongosh
use hrms_ai
db.document_chunks.getSearchIndexes()
// Should show vector_index with status "READY"

Lock

  • Vector search index created

Checkpoint

  • Index shows "READY" in Atlas UI
  • Index name is vector_index
  • Type "GATE 06 PASSED" to continue

Step 07: Create Redis (Memorystore) Instance

Input

  • Step 06 complete
  • GCP project with APIs enabled

Constraints

  • Basic tier (no HA for MVP)
  • 1GB capacity
  • Same region as Cloud Run

Task

# Create VPC connector first (needed for Cloud Run to reach Redis)
gcloud compute networks vpc-access connectors create hrms-connector \
  --region=$REGION \
  --range=10.8.0.0/28

# Create Redis instance
gcloud redis instances create hrms-cache \
  --size=1 \
  --region=$REGION \
  --redis-version=redis_7_0 \
  --tier=basic

# Get Redis host and port
gcloud redis instances describe hrms-cache --region=$REGION \
  --format="value(host,port)"
# Save this for REDIS_URL

Gate

# Verify Redis is running
gcloud redis instances describe hrms-cache --region=$REGION \
  --format="value(state)"
# Should return: READY

# Verify VPC connector
gcloud compute networks vpc-access connectors describe hrms-connector \
  --region=$REGION --format="value(state)"
# Should return: READY

Lock

  • Redis instance hrms-cache created
  • VPC connector hrms-connector created

Checkpoint

  • Redis shows READY state
  • VPC connector shows READY state
  • Redis host/port saved
  • Type "GATE 07 PASSED" to continue

Step 08: Create Cloud Storage Bucket

Input

  • Step 07 complete

Constraints

  • Regional bucket (same region as services)
  • Standard storage class
  • Uniform bucket-level access

Task

# Create bucket for document storage
gcloud storage buckets create gs://hrms-documents-prod \
  --location=$REGION \
  --default-storage-class=STANDARD \
  --uniform-bucket-level-access

# Create bucket for staging
gcloud storage buckets create gs://hrms-documents-staging \
  --location=$REGION \
  --default-storage-class=STANDARD \
  --uniform-bucket-level-access

Gate

# Verify buckets exist
gcloud storage buckets list --filter="name:hrms-documents"
# Should show both buckets

Lock

  • Storage buckets created

Checkpoint

  • Both buckets created
  • Uniform access enabled
  • Type "GATE 08 PASSED" to continue

Step 09: Create Secrets in Secret Manager

Input

  • Step 08 complete
  • All credentials from previous steps

Constraints

  • One secret per sensitive value
  • Use consistent naming: hrms-{service}-{name}

Task

# Database URL (construct from Cloud SQL info)
# Format: postgresql://user:password@/database?host=/cloudsql/CONNECTION_NAME
echo -n "postgresql://hrms_app:<PASSWORD>@/hrms_prod?host=/cloudsql/bluewoo-hrms:europe-west1:hrms-db" | \
  gcloud secrets create hrms-database-url --data-file=-

# JWT Secret (generate random)
openssl rand -base64 32 | gcloud secrets create hrms-jwt-secret --data-file=-

# Auth.js Secret (generate random)
openssl rand -base64 32 | gcloud secrets create hrms-auth-secret --data-file=-

# Google OAuth credentials
echo -n "<GOOGLE_CLIENT_ID>" | gcloud secrets create hrms-google-client-id --data-file=-
echo -n "<GOOGLE_CLIENT_SECRET>" | gcloud secrets create hrms-google-client-secret --data-file=-

# MongoDB URI
echo -n "mongodb+srv://hrms_ai_app:<PASSWORD>@hrms-ai-prod.xxxxx.mongodb.net/hrms_ai" | \
  gcloud secrets create hrms-mongodb-uri --data-file=-

# OpenAI API Key
echo -n "sk-..." | gcloud secrets create hrms-openai-api-key --data-file=-

# Redis URL
echo -n "redis://<REDIS_HOST>:6379" | gcloud secrets create hrms-redis-url --data-file=-

Gate

# List all secrets
gcloud secrets list --filter="name:hrms"
# Should show all 8 secrets

# Verify a secret has a version
gcloud secrets versions list hrms-database-url
# Should show version 1 with state ENABLED

Lock

  • All secrets created in Secret Manager

Checkpoint

  • 8 secrets created
  • All secrets have version 1 enabled
  • Type "GATE 09 PASSED" to continue

Step 10: Create Service Account for Cloud Run

Input

  • Step 09 complete

Constraints

  • Least privilege: only permissions needed
  • One service account for all HRMS services

Task

# Create service account
gcloud iam service-accounts create hrms-runtime \
  --display-name="HRMS Runtime Service Account"

export SA_EMAIL="hrms-runtime@${PROJECT_ID}.iam.gserviceaccount.com"

# Grant Cloud SQL Client (for database connection)
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/cloudsql.client"

# Grant Secret Manager access
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/secretmanager.secretAccessor"

# Grant Storage access
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/storage.objectAdmin"

Gate

# Verify service account exists
gcloud iam service-accounts describe $SA_EMAIL
# Should show service account details

# Verify roles
gcloud projects get-iam-policy $PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:${SA_EMAIL}" \
  --format="table(bindings.role)"
# Should show cloudsql.client, secretmanager.secretAccessor, storage.objectAdmin

Lock

  • Service account hrms-runtime created with permissions

Checkpoint

  • Service account exists
  • 3 roles assigned
  • Type "GATE 10 PASSED" to continue

Step 11: Create GitHub Actions Service Account

Input

  • Step 10 complete

Constraints

  • Separate from runtime service account
  • Only deployment permissions

Task

# Create service account for GitHub Actions
gcloud iam service-accounts create github-actions \
  --display-name="GitHub Actions Deployer"

export DEPLOY_SA="github-actions@${PROJECT_ID}.iam.gserviceaccount.com"

# Grant Cloud Run Admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${DEPLOY_SA}" \
  --role="roles/run.admin"

# Grant Artifact Registry Writer
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${DEPLOY_SA}" \
  --role="roles/artifactregistry.writer"

# Grant Service Account User (to deploy with runtime SA)
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${DEPLOY_SA}" \
  --role="roles/iam.serviceAccountUser"

Gate

# Verify service account
gcloud iam service-accounts describe $DEPLOY_SA

# Verify roles (should have 3)
gcloud projects get-iam-policy $PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:${DEPLOY_SA}" \
  --format="table(bindings.role)"

Lock

  • GitHub Actions service account created

Checkpoint

  • Service account exists
  • 3 deployment roles assigned
  • Type "GATE 11 PASSED" to continue

Step 12: Configure Workload Identity Federation

Input

  • Step 11 complete
  • GitHub repository exists

Constraints

  • No service account keys (use OIDC)
  • Restrict to specific repository

Task

# Get project number
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")

# Create Workload Identity Pool
gcloud iam workload-identity-pools create github-pool \
  --location="global" \
  --display-name="GitHub Actions Pool"

# Create OIDC Provider
gcloud iam workload-identity-pools providers create-oidc github-provider \
  --location="global" \
  --workload-identity-pool="github-pool" \
  --display-name="GitHub Provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"

# Allow GitHub repo to use service account
# Replace YOUR_ORG/hrms with your actual repo
gcloud iam service-accounts add-iam-policy-binding $DEPLOY_SA \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.repository/YOUR_ORG/hrms"

# Get the provider resource name (save for GitHub secret)
echo "projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/providers/github-provider"

Gate

# Verify pool exists
gcloud iam workload-identity-pools describe github-pool --location=global

# Verify provider exists
gcloud iam workload-identity-pools providers describe github-provider \
  --workload-identity-pool=github-pool \
  --location=global

Lock

  • Workload Identity Federation configured

Checkpoint

  • Pool and provider created
  • Service account binding added
  • Provider resource name saved
  • Type "GATE 12 PASSED" to continue

Step 13: Add GitHub Repository Secrets

Input

  • Step 12 complete
  • Access to GitHub repository settings

Constraints

  • Use GitHub UI or CLI
  • Required secrets only

Task

Go to GitHub → Repository → Settings → Secrets → Actions

Add these secrets:

Secret NameValue
GCP_PROJECT_IDbluewoo-hrms
GCP_REGIONeurope-west1
GCP_WORKLOAD_IDENTITY_PROVIDERprojects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider
GCP_SERVICE_ACCOUNTgithub-actions@bluewoo-hrms.iam.gserviceaccount.com

Gate

# Using GitHub CLI
gh secret list
# Should show all 4 secrets

Or verify in GitHub UI: Settings → Secrets shows 4 secrets.

Lock

  • GitHub secrets configured

Checkpoint

  • 4 secrets added to GitHub
  • No plain text credentials in code
  • Type "GATE 13 PASSED" to continue

Step 14: Create Production GitHub Environment

Input

  • Step 13 complete

Constraints

  • Require manual approval for production
  • Add yourself as reviewer

Task

In GitHub → Repository → Settings → Environments:

  1. Click New environment

  2. Name: production

  3. Configure:

    • ✅ Required reviewers: Add yourself
    • ⬜ Wait timer: 0 minutes (optional: add 5 min)
    • ✅ Deployment branches: main only
  4. Create another environment: staging

    • No protection rules (auto-deploy)
    • Deployment branches: main only

Gate

Verify in GitHub UI:

  • production environment exists with approval gate
  • staging environment exists without approval

Lock

  • GitHub environments configured

Checkpoint

  • production environment with approval
  • staging environment without approval
  • Type "GATE 14 PASSED" to continue

Step 15: Create Multi-Service CI/CD Workflow

Input

  • Step 14 complete
  • All secrets configured

Constraints

  • Single workflow file
  • Deploy all 3 services
  • Staging auto-deploy, production manual

Task

Create .github/workflows/deploy-hrms.yml:

name: Deploy HRMS

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
  REGION: ${{ secrets.GCP_REGION }}
  REGISTRY: ${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/hrms-images

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npm run types:check

      - name: Test
        run: npm test

  build-images:
    needs: build-and-test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    outputs:
      web-image: ${{ steps.build.outputs.web-image }}
      api-image: ${{ steps.build.outputs.api-image }}
      ai-image: ${{ steps.build.outputs.ai-image }}
    steps:
      - uses: actions/checkout@v4

      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Configure Docker
        run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev

      - name: Build and push images
        id: build
        run: |
          # Build web
          docker build -t ${{ env.REGISTRY }}/hrms-web:${{ github.sha }} -f apps/web/Dockerfile .
          docker push ${{ env.REGISTRY }}/hrms-web:${{ github.sha }}
          echo "web-image=${{ env.REGISTRY }}/hrms-web:${{ github.sha }}" >> $GITHUB_OUTPUT

          # Build api
          docker build -t ${{ env.REGISTRY }}/hrms-api:${{ github.sha }} -f apps/api/Dockerfile .
          docker push ${{ env.REGISTRY }}/hrms-api:${{ github.sha }}
          echo "api-image=${{ env.REGISTRY }}/hrms-api:${{ github.sha }}" >> $GITHUB_OUTPUT

          # Build ai
          docker build -t ${{ env.REGISTRY }}/hrms-ai:${{ github.sha }} -f apps/ai/Dockerfile .
          docker push ${{ env.REGISTRY }}/hrms-ai:${{ github.sha }}
          echo "ai-image=${{ env.REGISTRY }}/hrms-ai:${{ github.sha }}" >> $GITHUB_OUTPUT

  deploy-staging:
    needs: build-images
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Deploy API to staging
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-api-staging
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.api-image }}
          flags: |
            --service-account=hrms-runtime@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
            --vpc-connector=hrms-connector
            --set-secrets=DATABASE_URL=hrms-database-url:latest,JWT_SECRET=hrms-jwt-secret:latest,REDIS_URL=hrms-redis-url:latest
            --add-cloudsql-instances=${{ env.PROJECT_ID }}:${{ env.REGION }}:hrms-db

      - name: Deploy Web to staging
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-web-staging
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.web-image }}
          env_vars: |
            NEXT_PUBLIC_API_URL=https://hrms-api-staging-xxxxx.run.app
          flags: |
            --set-secrets=AUTH_SECRET=hrms-auth-secret:latest,GOOGLE_CLIENT_ID=hrms-google-client-id:latest,GOOGLE_CLIENT_SECRET=hrms-google-client-secret:latest

      - name: Deploy AI to staging
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-ai-staging
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.ai-image }}
          flags: |
            --set-secrets=MONGODB_URI=hrms-mongodb-uri:latest,OPENAI_API_KEY=hrms-openai-api-key:latest

  deploy-production:
    needs: [build-images, deploy-staging]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Deploy API to production
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-api
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.api-image }}
          flags: |
            --service-account=hrms-runtime@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
            --vpc-connector=hrms-connector
            --set-secrets=DATABASE_URL=hrms-database-url:latest,JWT_SECRET=hrms-jwt-secret:latest,REDIS_URL=hrms-redis-url:latest
            --add-cloudsql-instances=${{ env.PROJECT_ID }}:${{ env.REGION }}:hrms-db

      - name: Deploy Web to production
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-web
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.web-image }}
          env_vars: |
            NEXT_PUBLIC_API_URL=https://api.hrms.bluewoo.com
            AUTH_URL=https://app.hrms.bluewoo.com
          flags: |
            --set-secrets=AUTH_SECRET=hrms-auth-secret:latest,GOOGLE_CLIENT_ID=hrms-google-client-id:latest,GOOGLE_CLIENT_SECRET=hrms-google-client-secret:latest

      - name: Deploy AI to production
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-ai
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.ai-image }}
          flags: |
            --set-secrets=MONGODB_URI=hrms-mongodb-uri:latest,OPENAI_API_KEY=hrms-openai-api-key:latest

Gate

# Commit and push
git add .github/workflows/deploy-hrms.yml
git commit -m "Add multi-service CI/CD workflow"
git push origin main

# Check Actions tab - workflow should start

Lock

  • .github/workflows/deploy-hrms.yml created

Checkpoint

  • Workflow file committed
  • GitHub Actions shows workflow running
  • Type "GATE 15 PASSED" to continue

Step 16: Run Database Migrations

Input

  • Step 15 complete
  • Cloud SQL instance running

Constraints

  • Run from Cloud Shell or local with Cloud SQL Proxy
  • Use production database

Task

# Option A: Using Cloud SQL Proxy (local machine)
# Download proxy: https://cloud.google.com/sql/docs/postgres/connect-auth-proxy

# Start proxy
cloud-sql-proxy bluewoo-hrms:europe-west1:hrms-db &

# Set DATABASE_URL for Prisma
export DATABASE_URL="postgresql://hrms_app:<PASSWORD>@localhost:5432/hrms_prod"

# Run migrations
cd packages/database
npx prisma migrate deploy

# Option B: Using Cloud Shell
# Go to cloud.google.com/shell
# Clone your repo and run migrations from there

Gate

# Verify tables exist
npx prisma studio
# Should open and show all tables from schema

# Or via psql
psql $DATABASE_URL -c "\dt"
# Should list all tables

Lock

  • Database schema migrated

Checkpoint

  • All tables created in production database
  • Prisma Studio shows tables
  • Type "GATE 16 PASSED" to continue

Step 17: Deploy to Staging and Verify

Input

  • Step 16 complete
  • GitHub Actions workflow configured

Constraints

  • First deployment may take longer
  • Verify all services start

Task

The staging deployment should have triggered from Step 15. If not:

# Trigger manually
git commit --allow-empty -m "Trigger staging deployment"
git push origin main

Watch GitHub Actions and wait for staging deployment to complete.

Gate

# Get staging URLs
gcloud run services list --region=$REGION
# Should show hrms-api-staging, hrms-web-staging, hrms-ai-staging

# Test health endpoints
curl https://hrms-api-staging-xxxxx.run.app/health
# Should return: {"status":"ok","database":"connected"}

curl https://hrms-web-staging-xxxxx.run.app
# Should return HTML

curl https://hrms-ai-staging-xxxxx.run.app/health
# Should return: {"status":"ok"}

Lock

  • Staging services deployed

Checkpoint

  • All 3 staging services running
  • Health checks pass
  • No errors in Cloud Run logs
  • Type "GATE 17 PASSED" to continue

Step 18: Configure DNS Records

Input

  • Step 17 complete
  • DNS access (Namecheap)

Constraints

  • Use CNAME records
  • Point to Google's hosted domain service

Task

In Namecheap → Domain List → bluewoo.com → Advanced DNS:

Add CNAME records:

HostValueType
app.hrmsghs.googlehosted.com.CNAME
api.hrmsghs.googlehosted.com.CNAME
ai.hrmsghs.googlehosted.com.CNAME
app-staging.hrmsghs.googlehosted.com.CNAME
api-staging.hrmsghs.googlehosted.com.CNAME

Gate

# Check DNS propagation (may take minutes to hours)
dig app.hrms.bluewoo.com CNAME
# Should return ghs.googlehosted.com

Lock

  • DNS records configured

Checkpoint

  • All CNAME records added
  • DNS propagation started
  • Type "GATE 18 PASSED" to continue

Step 19: Map Custom Domains to Cloud Run

Input

  • Step 18 complete
  • DNS records propagated

Constraints

  • Must verify domain ownership first
  • SSL certificates auto-provisioned

Task

# Verify domain ownership (one-time)
gcloud domains verify bluewoo.com

# Map production domains
gcloud run domain-mappings create \
  --service=hrms-web \
  --domain=app.hrms.bluewoo.com \
  --region=$REGION

gcloud run domain-mappings create \
  --service=hrms-api \
  --domain=api.hrms.bluewoo.com \
  --region=$REGION

gcloud run domain-mappings create \
  --service=hrms-ai \
  --domain=ai.hrms.bluewoo.com \
  --region=$REGION

# Map staging domains
gcloud run domain-mappings create \
  --service=hrms-web-staging \
  --domain=app-staging.hrms.bluewoo.com \
  --region=$REGION

gcloud run domain-mappings create \
  --service=hrms-api-staging \
  --domain=api-staging.hrms.bluewoo.com \
  --region=$REGION

Gate

# Check domain mappings
gcloud run domain-mappings list --region=$REGION

# Check certificate status (may take up to 24h)
gcloud run domain-mappings describe \
  --domain=app.hrms.bluewoo.com \
  --region=$REGION \
  --format="value(status.certificateStatus)"
# Should eventually show: CERTIFICATE_READY

Lock

  • Custom domains mapped

Checkpoint

  • All domain mappings created
  • SSL certificates provisioning
  • Type "GATE 19 PASSED" to continue

Step 20: Approve Production Deployment and Final Verification

Input

  • Step 19 complete
  • Staging verified working

Constraints

  • Approve in GitHub UI
  • Test all critical paths

Task

  1. Go to GitHub → Actions → Latest workflow run
  2. Click Review deployments for deploy-production job
  3. Click Approve and deploy
  4. Wait for deployment to complete

Gate

# Test production endpoints
curl https://api.hrms.bluewoo.com/health
# Should return: {"status":"ok","database":"connected"}

curl https://app.hrms.bluewoo.com
# Should return HTML (login page)

curl https://ai.hrms.bluewoo.com/health
# Should return: {"status":"ok"}

# Test auth flow
# Open https://app.hrms.bluewoo.com in browser
# Click "Sign in with Google"
# Verify redirect and login works

Lock

  • Production deployment complete

Checkpoint

  • Production deployment approved
  • All 3 services running
  • SSL certificates active
  • Auth flow works
  • Type "GATE 20 PASSED - PHASE 11 COMPLETE" to continue

Phase Complete Verification

All Services Running

gcloud run services list --region=$REGION
# Should show 6 services (3 staging + 3 production)

All Databases Connected

  • Cloud SQL: API service connects
  • MongoDB Atlas: AI service connects
  • Redis: API service for caching

All Domains Working

Cost Monitoring

Set up billing alerts:

gcloud billing budgets create \
  --billing-account=<BILLING_ACCOUNT_ID> \
  --display-name="HRMS Monthly Budget" \
  --budget-amount=200 \
  --threshold-rules=threshold-percent=0.5 \
  --threshold-rules=threshold-percent=0.9 \
  --threshold-rules=threshold-percent=1.0

Rollback Procedures

Rollback Cloud Run Service

# List revisions
gcloud run revisions list --service=hrms-api --region=$REGION

# Route traffic to previous revision
gcloud run services update-traffic hrms-api \
  --to-revisions=hrms-api-00005-abc=100 \
  --region=$REGION

Rollback Database Migration

# List migrations
npx prisma migrate status

# Rollback last migration (destructive!)
npx prisma migrate reset --skip-seed
npx prisma migrate deploy --to <previous-migration-name>

Next Steps

After Phase 11:

  1. Set up monitoring: Cloud Monitoring dashboards
  2. Configure alerts: Error rate, latency alerts
  3. Enable logging: Structured logging to Cloud Logging
  4. Security audit: Review IAM permissions
  5. Load testing: Verify scale under load

On this page

Phase 11: Production DeploymentPhase Context (READ FIRST)What This Phase AccomplishesWhat This Phase Does NOT IncludePrerequisitesArchitecture OverviewEnvironment Variables ReferenceWeb Service (Next.js)API Service (NestJS)AI Service (Express)Step 01: Create GCP Project and Enable APIsInputConstraintsTaskGateCommon ErrorsLockCheckpointStep 02: Create Artifact RegistryInputConstraintsTaskGateLockCheckpointStep 03: Create Cloud SQL PostgreSQL InstanceInputConstraintsTaskGateCommon ErrorsLockCheckpointStep 04: Create Production Database and UserInputConstraintsTaskGateLockCheckpointStep 05: Create MongoDB Atlas ClusterInputConstraintsTaskGateLockCheckpointStep 06: Create Vector Search Index in MongoDBInputConstraintsTaskGateLockCheckpointStep 07: Create Redis (Memorystore) InstanceInputConstraintsTaskGateLockCheckpointStep 08: Create Cloud Storage BucketInputConstraintsTaskGateLockCheckpointStep 09: Create Secrets in Secret ManagerInputConstraintsTaskGateLockCheckpointStep 10: Create Service Account for Cloud RunInputConstraintsTaskGateLockCheckpointStep 11: Create GitHub Actions Service AccountInputConstraintsTaskGateLockCheckpointStep 12: Configure Workload Identity FederationInputConstraintsTaskGateLockCheckpointStep 13: Add GitHub Repository SecretsInputConstraintsTaskGateLockCheckpointStep 14: Create Production GitHub EnvironmentInputConstraintsTaskGateLockCheckpointStep 15: Create Multi-Service CI/CD WorkflowInputConstraintsTaskGateLockCheckpointStep 16: Run Database MigrationsInputConstraintsTaskGateLockCheckpointStep 17: Deploy to Staging and VerifyInputConstraintsTaskGateLockCheckpointStep 18: Configure DNS RecordsInputConstraintsTaskGateLockCheckpointStep 19: Map Custom Domains to Cloud RunInputConstraintsTaskGateLockCheckpointStep 20: Approve Production Deployment and Final VerificationInputConstraintsTaskGateLockCheckpointPhase Complete VerificationAll Services RunningAll Databases ConnectedAll Domains WorkingCost MonitoringRollback ProceduresRollback Cloud Run ServiceRollback Database MigrationNext StepsRelated Documentation