xtablo-source/docs/CLOUD_BUILD_ENV_CONFIG.md
Arthur Belleville 247bc8b3af
Add docs
2025-11-14 09:14:25 +01:00

14 KiB

Cloud Build Environment Configuration

This guide explains how to set up separate staging and production triggers with environment-specific configurations.

Overview

The cloudbuild.yaml uses substitution variables to make it environment-agnostic. You'll create two separate triggers (one for staging, one for production) with different substitution variable values.

Note: The Dockerfile uses a single final stage. Environment differentiation (staging vs production) is handled entirely through Cloud Run environment variables, not at build time. This simplifies the build process - the same Docker image can be used for both environments.

Architecture

┌─────────────────┐      ┌──────────────────────┐
│  Git Branch     │      │  Cloud Build Trigger │
│  - main         │─────▶│  (Production)        │
│                 │      │  Variables: prod     │
└─────────────────┘      └──────────────────────┘

┌─────────────────┐      ┌──────────────────────┐
│  Git Branch     │      │  Cloud Build Trigger │
│  - develop      │─────▶│  (Staging)           │
│                 │      │  Variables: staging  │
└─────────────────┘      └──────────────────────┘

Required Substitution Variables

Common Variables (Different per Environment)

Variable Production Value Staging Value
$_NODE_ENV production staging
$_SERVICE_NAME xtablo-api xtablo-api-staging
$_SUPABASE_URL Production Supabase URL Staging Supabase URL
$_STREAM_CHAT_API_KEY Production key Staging key
$_EMAIL_USER noreply@xtablo.com staging@xtablo.com
$_EMAIL_CLIENT_ID Production OAuth client Staging OAuth client
$_R2_ACCOUNT_ID Production R2 account Staging R2 account
$_CORS_ORIGIN https://app.xtablo.com https://staging.xtablo.com
$_XTABLO_URL https://app.xtablo.com https://staging.xtablo.com
$_LOG_LEVEL info debug

Build Configuration (Same for Both)

Variable Value
$_AR_PROJECT_ID Your GCP project ID
$_AR_REPOSITORY xtablo (or your repository name)

Secret Manager References (Different per Environment)

Each environment should have its own secrets in Secret Manager:

Variable Production Secret Name Staging Secret Name
$_SUPABASE_SERVICE_ROLE_KEY_SECRET supabase-service-role-key-prod supabase-service-role-key-staging
$_SUPABASE_CONNECTION_STRING_SECRET supabase-connection-string-prod supabase-connection-string-staging
$_SUPABASE_CA_CERT_SECRET supabase-ca-cert-prod supabase-ca-cert-staging
$_STREAM_CHAT_API_SECRET_SECRET stream-chat-api-secret-prod stream-chat-api-secret-staging
$_STRIPE_SECRET_KEY_SECRET stripe-secret-key-prod stripe-secret-key-staging
$_STRIPE_WEBHOOK_SECRET_SECRET stripe-webhook-secret-prod stripe-webhook-secret-staging
$_EMAIL_CLIENT_SECRET_SECRET email-client-secret-prod email-client-secret-staging
$_EMAIL_REFRESH_TOKEN_SECRET email-refresh-token-prod email-refresh-token-staging
$_R2_ACCESS_KEY_ID_SECRET r2-access-key-id-prod r2-access-key-id-staging
$_R2_SECRET_ACCESS_KEY_SECRET r2-secret-access-key-prod r2-secret-access-key-staging

Setup Instructions

Step 1: Create Secrets in Secret Manager

Production Secrets

# Supabase (Production)
echo -n "prod-service-role-key" | gcloud secrets create supabase-service-role-key-prod --data-file=-
echo -n "prod-connection-string" | gcloud secrets create supabase-connection-string-prod --data-file=-
echo -n "prod-ca-cert" | gcloud secrets create supabase-ca-cert-prod --data-file=-

# Stream Chat (Production)
echo -n "prod-stream-secret" | gcloud secrets create stream-chat-api-secret-prod --data-file=-

# Stripe (Production)
echo -n "prod-stripe-key" | gcloud secrets create stripe-secret-key-prod --data-file=-
echo -n "prod-webhook-secret" | gcloud secrets create stripe-webhook-secret-prod --data-file=-

# Email (Production)
echo -n "prod-client-secret" | gcloud secrets create email-client-secret-prod --data-file=-
echo -n "prod-refresh-token" | gcloud secrets create email-refresh-token-prod --data-file=-

# R2 (Production)
echo -n "prod-access-key-id" | gcloud secrets create r2-access-key-id-prod --data-file=-
echo -n "prod-secret-access-key" | gcloud secrets create r2-secret-access-key-prod --data-file=-

Staging Secrets

# Supabase (Staging)
echo -n "staging-service-role-key" | gcloud secrets create supabase-service-role-key-staging --data-file=-
echo -n "staging-connection-string" | gcloud secrets create supabase-connection-string-staging --data-file=-
echo -n "staging-ca-cert" | gcloud secrets create supabase-ca-cert-staging --data-file=-

# Stream Chat (Staging)
echo -n "staging-stream-secret" | gcloud secrets create stream-chat-api-secret-staging --data-file=-

# Stripe (Staging - use test keys)
echo -n "staging-stripe-key" | gcloud secrets create stripe-secret-key-staging --data-file=-
echo -n "staging-webhook-secret" | gcloud secrets create stripe-webhook-secret-staging --data-file=-

# Email (Staging)
echo -n "staging-client-secret" | gcloud secrets create email-client-secret-staging --data-file=-
echo -n "staging-refresh-token" | gcloud secrets create email-refresh-token-staging --data-file=-

# R2 (Staging)
echo -n "staging-access-key-id" | gcloud secrets create r2-access-key-id-staging --data-file=-
echo -n "staging-secret-access-key" | gcloud secrets create r2-secret-access-key-staging --data-file=-

Step 2: Grant Access to Secrets

PROJECT_ID="your-project-id"
SERVICE_ACCOUNT="${PROJECT_ID}@appspot.gserviceaccount.com"

# Grant access to all production secrets
for secret in \
  supabase-service-role-key-prod \
  supabase-connection-string-prod \
  supabase-ca-cert-prod \
  stream-chat-api-secret-prod \
  stripe-secret-key-prod \
  stripe-webhook-secret-prod \
  email-client-secret-prod \
  email-refresh-token-prod \
  r2-access-key-id-prod \
  r2-secret-access-key-prod
do
  gcloud secrets add-iam-policy-binding $secret \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role="roles/secretmanager.secretAccessor"
done

# Grant access to all staging secrets
for secret in \
  supabase-service-role-key-staging \
  supabase-connection-string-staging \
  supabase-ca-cert-staging \
  stream-chat-api-secret-staging \
  stripe-secret-key-staging \
  stripe-webhook-secret-staging \
  email-client-secret-staging \
  email-refresh-token-staging \
  r2-access-key-id-staging \
  r2-secret-access-key-staging
do
  gcloud secrets add-iam-policy-binding $secret \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role="roles/secretmanager.secretAccessor"
done

Step 3: Create Cloud Build Triggers

Production Trigger

Via gcloud CLI:

gcloud builds triggers create github \
  --name="xtablo-api-production" \
  --repo-name="xtablo-source" \
  --repo-owner="your-github-org" \
  --branch-pattern="^main$" \
  --build-config="apps/api/cloudbuild.yaml" \
  --substitutions='
_NODE_ENV=production,
_SERVICE_NAME=xtablo-api,
_AR_PROJECT_ID=your-project-id,
_AR_REPOSITORY=xtablo,
_SUPABASE_URL=https://your-prod-project.supabase.co,
_STREAM_CHAT_API_KEY=your-prod-stream-key,
_EMAIL_USER=noreply@xtablo.com,
_EMAIL_CLIENT_ID=your-prod-oauth-client-id,
_R2_ACCOUNT_ID=your-prod-r2-account,
_CORS_ORIGIN=https://app.xtablo.com,
_XTABLO_URL=https://app.xtablo.com,
_LOG_LEVEL=info,
_SUPABASE_SERVICE_ROLE_KEY_SECRET=supabase-service-role-key-prod,
_SUPABASE_CONNECTION_STRING_SECRET=supabase-connection-string-prod,
_SUPABASE_CA_CERT_SECRET=supabase-ca-cert-prod,
_STREAM_CHAT_API_SECRET_SECRET=stream-chat-api-secret-prod,
_STRIPE_SECRET_KEY_SECRET=stripe-secret-key-prod,
_STRIPE_WEBHOOK_SECRET_SECRET=stripe-webhook-secret-prod,
_EMAIL_CLIENT_SECRET_SECRET=email-client-secret-prod,
_EMAIL_REFRESH_TOKEN_SECRET=email-refresh-token-prod,
_R2_ACCESS_KEY_ID_SECRET=r2-access-key-id-prod,
_R2_SECRET_ACCESS_KEY_SECRET=r2-secret-access-key-prod
'

Staging Trigger

Via gcloud CLI:

gcloud builds triggers create github \
  --name="xtablo-api-staging" \
  --repo-name="xtablo-source" \
  --repo-owner="your-github-org" \
  --branch-pattern="^develop$" \
  --build-config="apps/api/cloudbuild.yaml" \
  --substitutions='
_NODE_ENV=staging,
_SERVICE_NAME=xtablo-api-staging,
_AR_PROJECT_ID=your-project-id,
_AR_REPOSITORY=xtablo,
_SUPABASE_URL=https://your-staging-project.supabase.co,
_STREAM_CHAT_API_KEY=your-staging-stream-key,
_EMAIL_USER=staging@xtablo.com,
_EMAIL_CLIENT_ID=your-staging-oauth-client-id,
_R2_ACCOUNT_ID=your-staging-r2-account,
_CORS_ORIGIN=https://staging.xtablo.com,
_XTABLO_URL=https://staging.xtablo.com,
_LOG_LEVEL=debug,
_SUPABASE_SERVICE_ROLE_KEY_SECRET=supabase-service-role-key-staging,
_SUPABASE_CONNECTION_STRING_SECRET=supabase-connection-string-staging,
_SUPABASE_CA_CERT_SECRET=supabase-ca-cert-staging,
_STREAM_CHAT_API_SECRET_SECRET=stream-chat-api-secret-staging,
_STRIPE_SECRET_KEY_SECRET=stripe-secret-key-staging,
_STRIPE_WEBHOOK_SECRET_SECRET=stripe-webhook-secret-staging,
_EMAIL_CLIENT_SECRET_SECRET=email-client-secret-staging,
_EMAIL_REFRESH_TOKEN_SECRET=email-refresh-token-staging,
_R2_ACCESS_KEY_ID_SECRET=r2-access-key-id-staging,
_R2_SECRET_ACCESS_KEY_SECRET=r2-secret-access-key-staging
'

Step 4: Configure via Console (Alternative)

If you prefer using the Google Cloud Console:

  1. Go to Cloud Build > Triggers

  2. Click Create Trigger

  3. Configure:

    • Name: xtablo-api-production (or xtablo-api-staging)
    • Event: Push to a branch
    • Repository: Your GitHub repository
    • Branch: ^main$ (or ^develop$ for staging)
    • Build configuration: Cloud Build configuration file
    • Location: apps/api/cloudbuild.yaml
  4. Scroll to Substitution variables and add all variables from the table above

  5. Save the trigger

Verification

Test Production Deployment

# Trigger manually
gcloud builds triggers run xtablo-api-production --branch=main

# Check logs
gcloud builds list --limit=5
gcloud builds log BUILD_ID

# Verify deployment
gcloud run services describe xtablo-api --region=europe-west1

Test Staging Deployment

# Trigger manually
gcloud builds triggers run xtablo-api-staging --branch=develop

# Check logs
gcloud builds list --limit=5

# Verify deployment
gcloud run services describe xtablo-api-staging --region=europe-west1

Verify Environment Variables

# Production
gcloud run services describe xtablo-api --region=europe-west1 \
  --format="value(spec.template.spec.containers[0].env)"

# Staging
gcloud run services describe xtablo-api-staging --region=europe-west1 \
  --format="value(spec.template.spec.containers[0].env)"

Best Practices

  1. Secret Naming: Use -prod and -staging suffixes for secrets
  2. Service Names: Use different names for each environment (xtablo-api vs xtablo-api-staging)
  3. Branches: Deploy production from main, staging from develop
  4. Stripe Keys: Use Stripe test keys in staging
  5. Supabase: Use separate Supabase projects for staging and production
  6. Monitoring: Set up different alert thresholds for staging vs production
  7. Resource Limits: You may want lower limits for staging to save costs

Quick Reference

Environment Comparison

Aspect Production Staging
Branch main develop
Service Name xtablo-api xtablo-api-staging
Domain app.xtablo.com staging.xtablo.com
Log Level info debug
Stripe Live keys Test keys
Min Instances 1 (optional) 0
Secrets Suffix -prod -staging

Troubleshooting

"Secret not found" error

  • Ensure secret names in substitution variables match actual secret names
  • Verify secrets exist: gcloud secrets list | grep staging or grep prod

Wrong environment variables in deployment

  • Check trigger substitution variables in Cloud Build console
  • Verify the correct trigger is being used for the branch

Services pointing to wrong databases

  • Ensure SUPABASE_URL is different for staging/production
  • Check that secret names include correct environment suffix

Example: Adding a New Environment Variable

To add a new environment variable (e.g., FEATURE_FLAG):

  1. Add to cloudbuild.yaml in the --set-env-vars line:

    "NODE_ENV=$_NODE_ENV,...,FEATURE_FLAG=$_FEATURE_FLAG"
    
  2. Add to both triggers with different values:

    • Production: _FEATURE_FLAG=false
    • Staging: _FEATURE_FLAG=true
  3. Update app code to read process.env.FEATURE_FLAG