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.
| Attribute | Value |
|---|---|
| Steps | 01-20 |
| Estimated Time | 12-16 hours |
| Dependencies | Phase 10.5 complete (Pre-Launch Features) |
| Completion Gate | All 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:
- GCP Account: With billing enabled
- MongoDB Atlas Account: Free tier is fine initially
- Domain Access: DNS management for
bluewoo.com(Namecheap) - GitHub Repository: With Actions enabled
- gcloud CLI: Installed and authenticated
- 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.comAPI 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.comAI Service (Express)
# MongoDB
MONGODB_URI=<from-secret-manager>
# OpenAI
OPENAI_API_KEY=<from-secret-manager>
# Backend
HRMS_API_URL=https://api.hrms.bluewoo.comStep 01: Create GCP Project and Enable APIs
Input
- GCP account with billing enabled
gcloudCLI 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.comGate
# Verify APIs enabled
gcloud services list --enabled | grep -E "(run|sql|artifact|secret|redis|storage|vpc)"
# Should show all 7 APIs enabledCommon Errors
| Error | Cause | Fix |
|---|---|---|
billing account not found | Wrong billing ID | Run gcloud billing accounts list to find correct ID |
permission denied | Not owner of project | Ensure you're project owner or have roles/owner |
quota exceeded | Too many projects | Delete unused projects or request quota increase |
Lock
- GCP project created
- APIs enabled
Checkpoint
-
gcloud config get-value projectreturnsbluewoo-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.devGate
# Verify repository exists
gcloud artifacts repositories list --location=$REGION
# Should show hrms-images repositoryLock
hrms-imagesrepository 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: RUNNABLECommon Errors
| Error | Cause | Fix |
|---|---|---|
instance creation failed | Quota exceeded | Check SQL instance quota in IAM |
timeout | Network issues | Wait and retry, Cloud SQL takes time |
Lock
- Cloud SQL instance
hrms-dbcreated
Checkpoint
-
gcloud sql instances describe hrms-dbshows 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-dbGate
# 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 postgresLock
- Database
hrms_prodcreated - User
hrms_appcreated
Checkpoint
- Database
hrms_prodexists - User
hrms_appexists - 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
- Go to MongoDB Atlas
- Create new project:
Bluewoo HRMS - Create cluster:
- Tier: M0 (free) for staging, M10 for production
- Provider: Google Cloud
- Region: Belgium (europe-west1)
- Cluster Name:
hrms-ai-stagingorhrms-ai-prod
- Create database user:
- Username:
hrms_ai_app - Password: Generate strong password
- Roles:
readWriteonhrms_aidatabase
- Username:
- Configure Network Access:
- Add
0.0.0.0/0for now (we'll restrict later) - Or add GCP Cloud Run egress IPs
- Add
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:
- Go to Database → Browse Collections
- Create database:
hrms_ai - Create collection:
document_chunks - Go to Search tab → Create Search Index
- Use JSON Editor and paste:
{
"mappings": {
"dynamic": true,
"fields": {
"embedding": {
"type": "knnVector",
"dimensions": 1536,
"similarity": "cosine"
},
"tenantId": {
"type": "string"
}
}
}
}- Name the index:
vector_index - 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_URLGate
# 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: READYLock
- Redis instance
hrms-cachecreated - VPC connector
hrms-connectorcreated
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-accessGate
# Verify buckets exist
gcloud storage buckets list --filter="name:hrms-documents"
# Should show both bucketsLock
- 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 ENABLEDLock
- 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.objectAdminLock
- Service account
hrms-runtimecreated 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=globalLock
- 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 Name | Value |
|---|---|
GCP_PROJECT_ID | bluewoo-hrms |
GCP_REGION | europe-west1 |
GCP_WORKLOAD_IDENTITY_PROVIDER | projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider |
GCP_SERVICE_ACCOUNT | github-actions@bluewoo-hrms.iam.gserviceaccount.com |
Gate
# Using GitHub CLI
gh secret list
# Should show all 4 secretsOr 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:
-
Click New environment
-
Name:
production -
Configure:
- ✅ Required reviewers: Add yourself
- ⬜ Wait timer: 0 minutes (optional: add 5 min)
- ✅ Deployment branches:
mainonly
-
Create another environment:
staging- No protection rules (auto-deploy)
- Deployment branches:
mainonly
Gate
Verify in GitHub UI:
productionenvironment exists with approval gatestagingenvironment exists without approval
Lock
- GitHub environments configured
Checkpoint
-
productionenvironment with approval -
stagingenvironment 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:latestGate
# 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 startLock
.github/workflows/deploy-hrms.ymlcreated
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 thereGate
# 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 tablesLock
- 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 mainWatch 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:
| Host | Value | Type |
|---|---|---|
app.hrms | ghs.googlehosted.com. | CNAME |
api.hrms | ghs.googlehosted.com. | CNAME |
ai.hrms | ghs.googlehosted.com. | CNAME |
app-staging.hrms | ghs.googlehosted.com. | CNAME |
api-staging.hrms | ghs.googlehosted.com. | CNAME |
Gate
# Check DNS propagation (may take minutes to hours)
dig app.hrms.bluewoo.com CNAME
# Should return ghs.googlehosted.comLock
- 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=$REGIONGate
# 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_READYLock
- 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
- Go to GitHub → Actions → Latest workflow run
- Click Review deployments for
deploy-productionjob - Click Approve and deploy
- 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 worksLock
- 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
| URL | Service |
|---|---|
| https://app.hrms.bluewoo.com | Web frontend |
| https://api.hrms.bluewoo.com | NestJS backend |
| https://ai.hrms.bluewoo.com | AI service |
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.0Rollback 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=$REGIONRollback 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:
- Set up monitoring: Cloud Monitoring dashboards
- Configure alerts: Error rate, latency alerts
- Enable logging: Structured logging to Cloud Logging
- Security audit: Review IAM permissions
- Load testing: Verify scale under load
Related Documentation
- Cloud SQL Setup - Detailed PostgreSQL guide
- MongoDB Atlas Setup - Vector DB configuration
- Secret Manager Setup - Credential management
- Redis Setup - Caching configuration
- Multi-Service CI/CD - GitHub Actions details