Deploy SupportPilot to Google Cloud

Cloud Run runs the container serverlessly (scale to zero, pay per request), Cloud SQL stores LangGraph checkpoints, Secret Manager holds keys, and GitHub Actions ships every push.

Your progress
GitHub Actions Users / frontend Artifact Registry Cloud Runsupportpilot · SA identity Secret Manager Cloud SQL (Postgres) LLM API / Vertex AI

Prerequisites

Step-by-step

1

Set variables and enable APIs

export PROJECT_ID=my-agent-project
export REGION=us-central1
gcloud auth login
gcloud config set project $PROJECT_ID

gcloud services enable run.googleapis.com artifactregistry.googleapis.com \
  cloudbuild.googleapis.com sqladmin.googleapis.com secretmanager.googleapis.com \
  iamcredentials.googleapis.com
2

Build and push the image

Cloud Build builds remotely — no local Docker needed and the image is always linux/amd64.

gcloud artifacts repositories create agents \
  --repository-format=docker --location=$REGION

export IMAGE=$REGION-docker.pkg.dev/$PROJECT_ID/agents/supportpilot:v1
gcloud builds submit --tag $IMAGE .
3

Create Cloud SQL for Postgres

export DB_PASS=$(openssl rand -hex 16)      # hex → no URL-encoding surprises

gcloud sql instances create supportpilot-db \
  --database-version=POSTGRES_16 --edition=enterprise \
  --tier=db-custom-1-3840 --region=$REGION

gcloud sql databases create agent --instance=supportpilot-db
gcloud sql users create agent --instance=supportpilot-db --password="$DB_PASS"

export INSTANCE_CONN=$(gcloud sql instances describe supportpilot-db \
  --format='value(connectionName)')          # project:region:instance
How Cloud Run connectsWith --add-cloudsql-instances, Cloud Run mounts a Unix socket at /cloudsql/INSTANCE_CONN. libpq/psycopg accept it via the host query parameter, so no IP allow-listing is needed.
4

Store secrets in Secret Manager

printf '%s' "$ANTHROPIC_API_KEY" | gcloud secrets create anthropic-api-key --data-file=-

printf '%s' "postgresql://agent:${DB_PASS}@/agent?host=/cloudsql/${INSTANCE_CONN}" \
  | gcloud secrets create database-url --data-file=-
5

Create a least-privilege service account

gcloud iam service-accounts create supportpilot-run
export SA=supportpilot-run@$PROJECT_ID.iam.gserviceaccount.com

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member=serviceAccount:$SA --role=roles/cloudsql.client

for s in anthropic-api-key database-url; do
  gcloud secrets add-iam-policy-binding $s \
    --member=serviceAccount:$SA --role=roles/secretmanager.secretAccessor
done
6

Deploy to Cloud Run

gcloud run deploy supportpilot \
  --image=$IMAGE --region=$REGION \
  --service-account=$SA \
  --add-cloudsql-instances=$INSTANCE_CONN \
  --set-secrets=ANTHROPIC_API_KEY=anthropic-api-key:latest,DATABASE_URL=database-url:latest \
  --set-env-vars=MODEL=anthropic:claude-sonnet-5-5 \
  --cpu=1 --memory=1Gi --concurrency=40 --timeout=300 \
  --min-instances=0 --max-instances=10 \
  --no-allow-unauthenticated
  • --concurrency=40: agent requests are I/O-bound (waiting on the LLM), so one instance handles many.
  • --timeout=300: long agent runs and SSE streams need more than the default.
  • --min-instances=1 removes cold starts for production (costs a bit when idle).
  • --no-allow-unauthenticated: callers need an identity token. Put API Gateway / IAP / your backend in front for public use.
7

Smoke test

URL=$(gcloud run services describe supportpilot --region=$REGION --format='value(status.url)')
TOKEN=$(gcloud auth print-identity-token)

curl -s -H "Authorization: Bearer $TOKEN" $URL/healthz
curl -s -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  $URL/chat -d '{"thread_id":"gcp-1","message":"Order A1002 is late, refund it"}'
curl -s -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  $URL/approve -d '{"thread_id":"gcp-1","approved":true}'

gcloud run services logs read supportpilot --region=$REGION --limit=50
8

CI/CD with GitHub Actions (keyless)

Workload Identity Federation lets GitHub authenticate to GCP with short-lived OIDC tokens — no JSON keys in GitHub secrets.

gcloud iam service-accounts create github-deployer
export DEPLOYER=github-deployer@$PROJECT_ID.iam.gserviceaccount.com
for r in roles/run.admin roles/artifactregistry.writer roles/cloudbuild.builds.editor roles/iam.serviceAccountUser; do
  gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEPLOYER --role=$r
done

gcloud iam workload-identity-pools create github --location=global
gcloud iam workload-identity-pools providers create-oidc github \
  --location=global --workload-identity-pool=github \
  --issuer-uri=https://token.actions.githubusercontent.com \
  --attribute-mapping=google.subject=assertion.sub,attribute.repository=assertion.repository \
  --attribute-condition="assertion.repository=='YOUR_ORG/YOUR_REPO'"

export POOL=$(gcloud iam workload-identity-pools describe github --location=global --format='value(name)')
gcloud iam service-accounts add-iam-policy-binding $DEPLOYER \
  --role=roles/iam.workloadIdentityUser \
  --member="principalSet://iam.googleapis.com/$POOL/attribute.repository/YOUR_ORG/YOUR_REPO"
name: deploy-gcp
on:
  push:
    branches: [main]
permissions:
  contents: read
  id-token: write
env:
  REGION: us-central1
  IMAGE: us-central1-docker.pkg.dev/${{ vars.GCP_PROJECT }}/agents/supportpilot:${{ github.sha }}
jobs:
  test-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -r requirements-dev.txt && pytest -q
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
          service_account: ${{ vars.GCP_DEPLOYER_SA }}
      - uses: google-github-actions/setup-gcloud@v2
      - run: gcloud builds submit --tag "$IMAGE" .
      - uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: supportpilot
          region: ${{ env.REGION }}
          image: ${{ env.IMAGE }}

Only the image changes on each deploy; secrets, Cloud SQL and the service account set in step 6 are kept.

Using Vertex AI models instead of an API key

On GCP you can call Gemini (and partner models such as Claude) through Vertex AI, authenticated by the service account — no API key to rotate.

# requirements.txt: add langchain-google-vertexai
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member=serviceAccount:$SA --role=roles/aiplatform.user

gcloud run services update supportpilot --region=$REGION \
  --update-env-vars=MODEL=google_vertexai:YOUR_GEMINI_MODEL_ID,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=$REGION \
  --remove-secrets=ANTHROPIC_API_KEY

Because the graph uses init_chat_model(settings.model), nothing else changes.

Alternatives on GCP

OptionWhen to choose it
Cloud Run (this guide)Default for HTTP agent APIs: serverless, scales to zero, simple.
Vertex AI Agent EngineManaged runtime for agents (LangGraph, ADK) with managed sessions and memory; less infra to own.
GKE (Autopilot)You already run Kubernetes, need GPUs for self-hosted models, or sidecars/custom networking.
Cloud Run jobsBatch agents (nightly ticket triage) and one-off DB migrations.

Production notes & cost

# clean up
gcloud run services delete supportpilot --region=$REGION
gcloud sql instances delete supportpilot-db
gcloud artifacts repositories delete agents --location=$REGION
gcloud secrets delete anthropic-api-key; gcloud secrets delete database-url