Google Cloud Run skill for Codex
Private by default, scaled on purpose. Codex deploys to Cloud Run with a dedicated runtime service account, private ingress and pinned image digests, then sizes concurrency and instance limits against the real downstream connection budget. Rollouts go through tagged no-traffic revisions with a one-line rollback, and batch work moves to sharded, idempotent Cloud Run jobs.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- gcp-cloud-run Installs to ~/.agents/skills/gcp-cloud-run/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install gcp-cloud-run The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project gcp-cloud-run Installs to .agents/skills/gcp-cloud-run/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/gcp-cloud-run.zipunzip -o gcp-cloud-run.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) gcp-cloud-run PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $gcp-cloud-run, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Deploys and tunes Google Cloud Run services and jobs with gcloud - container contract, concurrency, min and max instances, CPU allocation, startup probes, Secret Manager env vars, dedicated runtime service accounts, run.invoker IAM, ingress, Direct VPC egress and tagged canary revisions. Use when shipping a container or source directory to Cloud Run, fixing cold starts or 503s, splitting traffic between revisions, running batch work as a Cloud Run job, or locking down who can invoke a service.
Use it when
- A container or source folder needs to run on Cloud Run
- Cold starts, 503s or a DB connection storm trace back to Cloud Run scaling
- One service must call another privately with IAM
- A revision rollout needs a canary and a quick rollback
Use something else when
- You are writing a single event handler with the Functions Framework: use gcp-cloud-functions
- The workload needs Kubernetes primitives such as DaemonSets or custom controllers: use gcp-gke
- You only need to design service accounts and role bindings: use gcp-iam
How to check the result
The private URL returns 403 without a token and 200 with an identity token, the revision runs as the dedicated service account, and the traffic split matches the plan.
The mistake it prevents
Leaving max instances unbounded while each instance opens a DB pool, so a traffic spike exhausts the database's connection limit.
Done well means: the service runs as its own least-privilege service account, is private unless the user said “public”, scales within a budget the user agreed to, gets secrets from Secret Manager rather than plain env vars, and every rollout can be shifted back to the previous revision in one command.
Before changing anything
- Read
AGENTS.mdfor the project, region and deploy conventions (Makefile target, Cloud Build trigger, Terraform). If the service is managed by Terraform or aservice.yaml, change it there, not with ad-hocgcloudflags that the next apply will revert. - Inspect what exists (needs network and gcloud auth; ask before running if the sandbox blocks network):
gcloud config list --format='value(core.project,run.region)'
gcloud run services list --region="$REGION"
gcloud run services describe api --region="$REGION" --format=export > current.yaml
gcloud run revisions list --service=api --region="$REGION" --limit=5
--format=export gives a YAML you can diff and re-apply with
gcloud run services replace current.yaml. Keep it as the rollback reference.
Service or job?
| Workload | Use | Key settings |
|---|---|---|
| HTTP/gRPC/WebSocket server, responds to requests | Service | concurrency, min/max instances, timeout |
| Batch, migration, nightly export, queue drain that ends | Job | tasks, parallelism, max retries, task timeout |
| Pull-based consumer with no HTTP endpoint, always on | Worker pool, or a service with instance-based billing and min instances | check worker pool availability in the region |
| Event from Pub/Sub, Storage, Eventarc | Service with an Eventarc trigger or push subscription | authenticated push, idempotent handler |
Container contract
- Listen on
0.0.0.0:$PORT(default 8080). Never hard-code the port. - Be stateless: local disk is an in-memory filesystem that counts against memory.
- Handle
SIGTERM: the instance gets about 10 seconds to finish beforeSIGKILL. Flush logs and close DB pools there. - Log JSON to stdout with a
severityfield; Cloud Logging parses it into structured logs. - Build for
linux/amd64unless the user explicitly targets otherwise; an arm64 image built on an Apple Silicon laptop fails to start with an exec format error.
Deploy
# From source (Cloud Build + buildpacks or Dockerfile); needs Cloud Build and Artifact Registry APIs
gcloud run deploy api --source . --region="$REGION" \
--service-account=api-runtime@"$PROJECT".iam.gserviceaccount.com \
--no-allow-unauthenticated \
--set-secrets=DATABASE_URL=db-url:latest \
--set-env-vars=LOG_LEVEL=info
# From a prebuilt image (preferred in CI: build once, deploy the same digest everywhere)
gcloud run deploy api --region="$REGION" \
--image="$REGION-docker.pkg.dev/$PROJECT/apps/api@sha256:<digest>"
- Pin images by digest in production deploys, not
:latest. --set-env-varsreplaces the whole set;--update-env-varsmerges. Same split for--set-secrets/--update-secrets. Using--set-*by accident wipes existing values.- Pin secret versions (
db-url:3) for reproducible rollbacks;:latestis resolved when an instance starts, so long-lived instances keep the old value until replaced.
Production deploys are outward-facing. Show the user the exact command, the target project and region, and get confirmation before running it.
Scaling and cost decisions
| Symptom or need | Setting |
|---|---|
| Cold starts hurt p99 latency | --min-instances=1 (billed while idle) plus --cpu-boost |
| Handler is CPU-bound or not thread-safe | Lower --concurrency (default 80; 1 for single-threaded runtimes doing heavy work) |
| Background work after the response returns | --no-cpu-throttling (instance-based billing); otherwise CPU is throttled outside requests |
| Downstream DB has a connection cap | --max-instances x pool size per instance must stay below the DB limit |
| Long requests (exports, streaming) | --timeout up to 3600s; consider a job instead |
| Spiky traffic, cost matters most | Default request-based billing, min instances 0 |
Rules of thumb:
- Start with
--cpu=1 --memory=512Mi, then size from Cloud Monitoring (run.googleapis.com/container/memory/utilizations, CPU utilisation, instance count). - Always set
--max-instanceson anything that talks to a database or a paid API. Unbounded scale-out turns a traffic spike into a connection storm or a bill. - Memory above 4 GiB needs at least 2 vCPU; the CLI rejects invalid pairs.
Startup and health
Add a startup probe when boot takes more than a few seconds, so traffic waits for readiness. Probes are set in the service YAML:
spec:
template:
spec:
containers:
- image: REGION-docker.pkg.dev/PROJECT/apps/api@sha256:...
startupProbe:
httpGet: { path: /healthz/ready }
periodSeconds: 2
failureThreshold: 30
livenessProbe:
httpGet: { path: /healthz/live }
periodSeconds: 15
Liveness must check only the process itself, never the database; a DB blip should not restart every instance at once.
Access control
| Who calls it | Configuration |
|---|---|
| Public website or API | --allow-unauthenticated (only with explicit user approval) |
| Another Cloud Run service or GCP workload | Private; grant the caller’s SA roles/run.invoker; caller sends an ID token with the service URL as audience |
| Browser users of an internal app | Private behind IAP or an external load balancer, not allUsers |
| Pub/Sub push, Cloud Scheduler, Eventarc | Their SA gets roles/run.invoker; enable OIDC token on the push/job |
gcloud run services add-iam-policy-binding api --region="$REGION" \
--member=serviceAccount:worker@"$PROJECT".iam.gserviceaccount.com \
--role=roles/run.invoker
Network restriction is separate from IAM. --ingress=internal blocks the internet entirely;
--ingress=internal-and-cloud-load-balancing allows only your load balancer. For private
egress to Cloud SQL private IP, Memorystore or on-prem, use Direct VPC egress:
--network=default --subnet=run-subnet --vpc-egress=private-ranges-only.
Calling a private service from code (Node):
import { GoogleAuth } from "google-auth-library";
const auth = new GoogleAuth();
const client = await auth.getIdTokenClient(process.env.TARGET_URL); // audience = service URL
const res = await client.request({ url: `${process.env.TARGET_URL}/v1/jobs`, method: "POST", data });
Safe rollouts with tags
gcloud run deploy api --image="$IMAGE" --region="$REGION" --no-traffic --tag=canary
# smoke test https://canary---api-<hash>-<region>.a.run.app
gcloud run services update-traffic api --region="$REGION" --to-tags=canary=10
gcloud run services update-traffic api --region="$REGION" --to-latest # promote
gcloud run services update-traffic api --region="$REGION" --to-revisions=api-00041-abc=100 # roll back
Watch error rate and latency per revision in the Cloud Run metrics before each step.
Jobs
gcloud run jobs deploy reindex --image="$IMAGE" --region="$REGION" \
--tasks=20 --parallelism=5 --max-retries=2 --task-timeout=30m \
--service-account=reindex-runtime@"$PROJECT".iam.gserviceaccount.com
gcloud run jobs execute reindex --region="$REGION" --wait
- Each task reads
CLOUD_RUN_TASK_INDEXandCLOUD_RUN_TASK_COUNTto pick its shard. Tasks must be idempotent because retries rerun the whole task. - Schedule with Cloud Scheduler calling the Jobs API, with a dedicated SA holding
roles/run.invokeron the job. - A job that mutates production data (migrations, backfills) needs user confirmation before
execute.
Gotchas
- Default compute service account has Editor on the project in many orgs. Never ship with it; create one SA per service.
- 503 with “no available instance” usually means max instances reached or the container
crashed on start. Read
gcloud logging read 'resource.type="cloud_run_revision"' --limit=50. - Request-based billing throttles CPU after the response: fire-and-forget promises and background threads stall. Either await the work or switch billing mode.
- Global variables persist across requests on one instance; never keep per-user state there.
- WebSockets and streaming count against
--timeout; clients must reconnect. - A revision is immutable. Changing an env var creates a new revision and rolls traffic unless the service pins traffic to specific revisions.
Verification
gcloud run services describe api --region="$REGION" \
--format='value(status.url,status.latestReadyRevisionName,spec.template.spec.serviceAccountName)'
curl -sS -o /dev/null -w '%{http_code}\n' "$URL/healthz/ready" # 403 expected if private
curl -sS -H "Authorization: Bearer $(gcloud auth print-identity-token)" "$URL/healthz/ready"
gcloud run services get-iam-policy api --region="$REGION"
- Private services return 403 without a token and 200 with one.
- The runtime SA is the dedicated one, not
[email protected]. - Traffic split shows the intended percentages; the previous revision is still listed.
Report template
**Service:** api (europe-west1), revision api-00042-xyz, image @sha256:ab12...
**Access:** private; run.invoker for worker@ and scheduler@; ingress internal-and-cloud-load-balancing
**Scaling:** min 1 / max 20, concurrency 40, 1 vCPU / 1Gi, cpu-boost on
**Secrets:** DATABASE_URL -> db-url:3
**Traffic:** canary 10% -> 100% after 30 min, error rate flat
**Rollback:** gcloud run services update-traffic api --to-revisions=api-00041-abc=100 What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Google Cloud Run"
short_description: "Private by default, scaled on purpose"
default_prompt: "$gcp-cloud-run Deploy this Dockerfile to Cloud Run in europe-west1 as a private service" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.