Google Cloud is the default cloud of this curriculum because Cloud Run takes you from a container to a
public, auto-scaling HTTPS endpoint in a single command — no servers, no load balancer to wire, scale-to-zero
billing. This track moves from gcloud init to a deployed service, managed Postgres over the Cloud SQL
connector, least-privilege IAM, and CI/CD — tagged by level so you read only as deep as you need.
Install the gcloud CLI and pick a project
BeginnerInstall the Google Cloud CLI, log in, then set your active project and a default region.
Why a project and a region are the first two decisions
Every Google Cloud resource lives inside a project — the unit of billing, IAM, and quota. The gcloud
CLI is the canonical way to drive the platform from a terminal and from CI. A region (e.g.
europe-west1, us-central1) is a physical location; putting compute and its database in the same
region keeps latency low and avoids cross-region egress charges. Set both once as defaults so you don’t
repeat them on every command.
Authenticate and set defaults
# Verify the CLI is installed
gcloud version
# Log in and choose the active project
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
# Set default region/zone so later commands can omit them
gcloud config set run/region europe-west1
gcloud config set compute/region europe-west1Enable the APIs you are about to use
BeginnerTurn on the Cloud Run, Cloud Build, Artifact Registry, and Cloud SQL APIs for the project.
Why services are off until you enable them
On Google Cloud every API is disabled by default; you enable exactly the ones a project needs. This keeps the attack surface and billing footprint small and makes the project’s dependencies explicit. Enabling is idempotent — running it again on an already-enabled API is a no-op — so it is safe to keep in a setup script.
Enable the core APIs
gcloud services enable \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com \
sqladmin.googleapis.comDeploy a container to Cloud Run from source
BeginnerFrom your app folder, run gcloud run deploy and let Cloud Build build the image for you.
What 'deploy from source' actually does
gcloud run deploy --source . hands your directory to Cloud Build, which builds a container image (using
your Dockerfile, or buildpacks if there isn’t one), pushes it to Artifact Registry, and rolls it out as a
new Cloud Run revision. Cloud Run gives that service a managed HTTPS URL, terminates TLS for you, and
scales the number of instances with traffic — down to zero when idle, so an unused service costs
nothing. Your container must listen on the port named by the PORT env var (Cloud Run sets it, default
8080).
One-command deploy
# Run from the directory containing your Dockerfile / source
gcloud run deploy aurora-api \
--source . \
--region europe-west1 \
--allow-unauthenticated
# The command prints the service URL; smoke-test it:
curl -i "$(gcloud run services describe aurora-api \
--region europe-west1 --format='value(status.url)')/healthz"Agent prompt — paste into an agent with repo access
Role: Senior platform engineer working in this repo.
Context: A Go (or any) HTTP service in this repo listens on the PORT env var (default 8080) and serves GET /healthz returning 200 "ok". gcloud is installed, authenticated, project + region defaults are set.
Task: Add a deploy script ./deploy.sh that builds and deploys this service to Cloud Run from source.
Requirements:
- Use `gcloud run deploy aurora-api --source . --region europe-west1 --allow-unauthenticated`.
- The container must read the listen port from the PORT env var, not a hardcoded 8080 (Cloud Run injects PORT).
- Script is idempotent and re-runnable; set `set -euo pipefail`.
- Echo the resulting service URL at the end.
Tests / acceptance:
- `bash ./deploy.sh` completes and prints a https://*.run.app URL.
- `curl -s -o /dev/null -w "%{http_code}" "$URL/healthz"` prints 200.
Output: a unified diff plus a one-paragraph note on what scale-to-zero means for cold starts.Read logs and the service URL
BeginnerFetch the deployed service’s URL and tail its request logs from the terminal.
Where Cloud Run logs go, and how to see them
Cloud Run streams stdout/stderr from every instance to Cloud Logging automatically — structured JSON lines
are parsed into fields you can filter on. gcloud run services logs read pulls them without opening the
console, which is what you want from CI or a debugging session. The service’s public URL is part of its
status, so you can script against it rather than copy-pasting.
URL and logs from the CLI
# The public URL
gcloud run services describe aurora-api \
--region europe-west1 --format='value(status.url)'
# Recent logs (add --limit / --format as needed)
gcloud run services logs read aurora-api \
--region europe-west1 --limit 50Push images to Artifact Registry
IntermediateCreate a Docker repository in Artifact Registry, then build and push an image you control.
Why a registry of your own, and why Artifact Registry
Deploying from source is great to start, but a real pipeline builds an image once, tags it, scans it, and
deploys that exact digest. Artifact Registry is Google Cloud’s container/package registry — it replaces
the older Container Registry (gcr.io). You create a regional Docker repo, authenticate Docker against it
once, then push images named REGION-docker.pkg.dev/PROJECT/REPO/IMAGE:TAG. Deploying a specific digest
makes rollouts reproducible.
Create a repo, build, push, deploy that image
# Create a Docker-format repo (once)
gcloud artifacts repositories create aurora \
--repository-format=docker \
--location=europe-west1
# Let Docker authenticate to that host
gcloud auth configure-docker europe-west1-docker.pkg.dev
# Build, push, and deploy the explicit image
IMAGE=europe-west1-docker.pkg.dev/YOUR_PROJECT_ID/aurora/api:v1
docker build -t "$IMAGE" .
docker push "$IMAGE"
gcloud run deploy aurora-api --image "$IMAGE" --region europe-west1Agent prompt — paste into an agent with repo access
Role: Senior platform engineer in this repo.
Context: We want reproducible deploys: build an image, push to Artifact Registry, deploy that digest to Cloud Run. The Artifact Registry repo is `aurora` in europe-west1; the GCP project id is in env GCP_PROJECT.
Task: Write ./release.sh that builds, pushes, and deploys a tagged image, deploying by digest (not tag) to Cloud Run.
Requirements:
- Image name: europe-west1-docker.pkg.dev/$GCP_PROJECT/aurora/api:$TAG where TAG defaults to the short git sha.
- After `docker push`, resolve the pushed digest and deploy `...api@sha256:...` so the revision is immutable.
- `set -euo pipefail`; fail loudly if GCP_PROJECT is unset.
Tests / acceptance:
- `TAG=test bash ./release.sh` pushes an image visible in `gcloud artifacts docker images list europe-west1-docker.pkg.dev/$GCP_PROJECT/aurora`.
- The new Cloud Run revision references an @sha256 image, verified by `gcloud run services describe aurora-api --region europe-west1 --format='value(spec.template.spec.containers[0].image)'`.
Output: a unified diff plus a one-paragraph note on why digest pinning beats tag deploys.Provision managed Postgres with Cloud SQL
IntermediateCreate a Cloud SQL for PostgreSQL instance, a database, and an application user.
What Cloud SQL gives you over self-hosting Postgres
Cloud SQL is Google Cloud’s managed relational database for PostgreSQL and MySQL. It runs the patched,
backed-up, point-in-time-recoverable database so you don’t operate the VM, the failover, or the upgrades.
You pick a Postgres version and a machine tier; Google handles maintenance windows and replication. Put the
instance in the same region as Cloud Run to keep the hop short. The instance has a connection name of
the form PROJECT:REGION:INSTANCE — you’ll need it to connect.
Create instance, database, and user
# Create a Postgres 16 instance (smallest tier shown; size for your workload)
gcloud sql instances create aurora-pg \
--database-version=POSTGRES_16 \
--tier=db-f1-micro \
--region=europe-west1
# A database and an application user
gcloud sql databases create aurora --instance=aurora-pg
gcloud sql users create app_user --instance=aurora-pg --password='CHANGE_ME'
# The connection name you'll wire into Cloud Run (PROJECT:REGION:INSTANCE)
gcloud sql instances describe aurora-pg --format='value(connectionName)'Connect Cloud Run to Cloud SQL the right way
IntermediateAttach the Cloud SQL instance to your Cloud Run service and connect through the Cloud SQL connector — never over the public internet.
Why the connector, and why not a raw public IP
Cloud Run can attach a Cloud SQL instance via --add-cloudsql-instances. With that flag, Cloud Run mounts
a Unix socket at /cloudsql/CONNECTION_NAME and runs the Cloud SQL connector, which opens an
encrypted, authenticated tunnel to the instance — no public IP exposure, no IP allow-listing. Your app then
connects to the socket host (/cloudsql/PROJECT:REGION:INSTANCE) instead of a TCP address. This is the
secure default; reserve public IP + SSL certs for cases the connector can’t cover.
Attach the instance and pass the socket path
# CONNECTION_NAME looks like your-project:europe-west1:aurora-pg
gcloud run deploy aurora-api \
--source . \
--region europe-west1 \
--add-cloudsql-instances CONNECTION_NAME \
--set-env-vars "INSTANCE_CONNECTION_NAME=CONNECTION_NAME,DB_NAME=aurora,DB_USER=app_user"
# Inside the container, connect over the mounted socket:
# host=/cloudsql/CONNECTION_NAME dbname=$DB_NAME user=$DB_USERAgent prompt — paste into an agent with repo access
Role: Senior Go + GCP engineer in this repo.
Context: A Go service deploys to Cloud Run. Cloud SQL (Postgres) is attached via --add-cloudsql-instances, so a Unix socket exists at /cloudsql/$INSTANCE_CONNECTION_NAME. Env vars INSTANCE_CONNECTION_NAME, DB_NAME, DB_USER are set; DB_PASSWORD comes from Secret Manager (assume it is already in env for this task). The driver is github.com/jackc/pgx/v5.
Task: Implement OpenPool(ctx) (*pgxpool.Pool, error) that connects to Cloud SQL over the Unix socket when INSTANCE_CONNECTION_NAME is set, and falls back to a local DATABASE_URL otherwise (for `go test` on a dev machine).
Requirements:
- Socket mode builds a DSN with host=/cloudsql/$INSTANCE_CONNECTION_NAME, never a public TCP host.
- Never log the password; build the DSN with url.UserPassword or pgxpool.ParseConfig.
- Ping the pool once on open and return a wrapped error on failure.
Tests / acceptance:
- A unit test asserts that with INSTANCE_CONNECTION_NAME set, the generated DSN contains `host=/cloudsql/` and no `:5432` TCP host.
- `go test ./... -run TestDSN` passes (no live DB required for the DSN-shape test).
Output: a unified diff plus a one-paragraph note on why the socket path avoids exposing a public IP.Give the service its own least-privilege identity
IntermediateCreate a dedicated service account, grant it only the roles it needs, and run the service as that identity.
Why per-service service accounts beat the default
By default a Cloud Run service runs as the project’s default compute service account, which is broadly
privileged. The least-privilege practice is a dedicated service account per service, granted only the
specific IAM roles that service requires — for a DB-backed API, typically roles/cloudsql.client (to use
the connector) and roles/secretmanager.secretAccessor (to read its secrets). If the service is ever
compromised, the blast radius is just those grants. Bind roles to the service account, then deploy with
--service-account.
Create the SA, grant minimal roles, run as it
SA=aurora-api@YOUR_PROJECT_ID.iam.gserviceaccount.com
gcloud iam service-accounts create aurora-api \
--display-name="Aurora API runtime"
# Grant ONLY what the service needs
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:$SA" --role="roles/cloudsql.client"
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:$SA" --role="roles/secretmanager.secretAccessor"
# Run the service as that identity
gcloud run deploy aurora-api --source . --region europe-west1 \
--service-account "$SA"Agent prompt — paste into an agent with repo access
Role: Senior GCP security engineer in this repo.
Context: The Aurora API runs on Cloud Run and needs Cloud SQL access plus one secret. We want a dedicated, least-privilege runtime service account, all set up via gcloud in a re-runnable script.
Task: Write ./iam-setup.sh that creates the service account aurora-api and grants only roles/cloudsql.client and roles/secretmanager.secretAccessor, then prints the deploy flag to use.
Requirements:
- Read the project id from `gcloud config get-value project`; fail if empty.
- Idempotent: creating an existing SA or re-binding an existing role must not abort the script (handle the already-exists case).
- Do NOT grant any Owner/Editor/primitive role.
- Echo the exact `--service-account` flag value at the end.
Tests / acceptance:
- After running, `gcloud projects get-iam-policy $PROJECT --flatten='bindings[].members' --filter="bindings.members:serviceAccount:aurora-api@$PROJECT.iam.gserviceaccount.com" --format='value(bindings.role)'` lists exactly roles/cloudsql.client and roles/secretmanager.secretAccessor.
- The script exits 0 on a second run.
Output: a unified diff plus a one-paragraph note on why a per-service SA limits blast radius.Keep secrets in Secret Manager, not env files
AdvancedStore the database password in Secret Manager and mount it into Cloud Run as an environment variable.
Why a secret store beats plaintext env vars
Hardcoding a password into --set-env-vars puts it in the service config, command history, and CI logs.
Secret Manager stores the value encrypted, versioned, and access-controlled by IAM. Cloud Run can reference
a secret version directly with --set-secrets NAME=SECRET:VERSION, injecting it at runtime as an env var
(or a mounted file) without it ever appearing in the service spec. Pair this with the
roles/secretmanager.secretAccessor grant from the previous Step so only the service’s identity can read
it.
Create a secret and mount it
# Create the secret and add the first version from stdin
printf 'super-secret-password' | gcloud secrets create db-password \
--replication-policy=automatic --data-file=-
# Mount the latest version into Cloud Run as DB_PASSWORD
gcloud run deploy aurora-api --source . --region europe-west1 \
--service-account "$SA" \
--set-secrets "DB_PASSWORD=db-password:latest"Agent prompt — paste into an agent with repo access
Role: Senior GCP engineer in this repo.
Context: The DB password currently arrives via --set-env-vars (plaintext in the service config). We have Secret Manager enabled and a runtime SA with roles/secretmanager.secretAccessor. We want zero secrets in the service spec or in git.
Task: Migrate the DB password to Secret Manager and update the deploy command to mount it via --set-secrets, removing it from --set-env-vars.
Requirements:
- Create/rotate the secret `db-password`; the deploy uses `--set-secrets DB_PASSWORD=db-password:latest`.
- The app reads DB_PASSWORD from the environment unchanged (no code change to the read path).
- No password literal remains in any committed file or in `--set-env-vars`.
Tests / acceptance:
- `gcloud run services describe aurora-api --region europe-west1 --format=export` shows DB_PASSWORD sourced from a secretKeyRef, and no plaintext DB_PASSWORD value.
- `grep -ri 'super-secret-password' .` returns nothing.
Output: a unified diff of the deploy script/config plus a one-paragraph note on secret rotation.Automate deploys with Cloud Build
AdvancedAdd a cloudbuild.yaml that builds, pushes, and deploys on every push, triggered from your repo.
Why CI builds the image, not your laptop
A build pipeline makes releases reproducible and auditable: the same steps run for every commit, the image
is tagged with the commit sha, and nobody deploys an un-pushed local change. Cloud Build runs ordered steps
in containers — typically docker build → docker push to Artifact Registry → gcloud run deploy. A
build trigger connected to your Git repo runs the pipeline automatically on push to a branch, so a merge
becomes a deploy.
A minimal build → push → deploy pipeline
# cloudbuild.yaml
steps:
- name: gcr.io/cloud-builders/docker
args: ['build', '-t', '${_IMAGE}', '.']
- name: gcr.io/cloud-builders/docker
args: ['push', '${_IMAGE}']
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
entrypoint: gcloud
args: ['run', 'deploy', 'aurora-api', '--image', '${_IMAGE}',
'--region', 'europe-west1']
images: ['${_IMAGE}']
substitutions:
_IMAGE: europe-west1-docker.pkg.dev/${PROJECT_ID}/aurora/api:${SHORT_SHA}Agent prompt — paste into an agent with repo access
Role: Senior CI/CD engineer in this repo.
Context: Source builds with a Dockerfile. We have an Artifact Registry repo `aurora` in europe-west1 and a Cloud Run service `aurora-api`. We want a Cloud Build pipeline that tags images with the commit SHA and deploys on push.
Task: Add cloudbuild.yaml that runs docker build -> docker push -> gcloud run deploy, using ${SHORT_SHA} in the tag, plus a one-line gcloud command to create a GitHub-connected trigger on the main branch.
Requirements:
- Image: europe-west1-docker.pkg.dev/${PROJECT_ID}/aurora/api:${SHORT_SHA} via a substitution.
- Deploy step uses the Cloud SDK builder image; the deploy keeps the existing --add-cloudsql-instances and --service-account flags.
- The trigger command targets branch ^main$ and references cloudbuild.yaml.
Tests / acceptance:
- `gcloud builds submit --config cloudbuild.yaml --substitutions=SHORT_SHA=manual` completes green and produces a new Cloud Run revision.
- After a push to main, `gcloud builds list --limit 1` shows a SUCCESS build for that commit.
Output: a unified diff (cloudbuild.yaml + the trigger command) plus a one-paragraph note on rollback via revisions.Roll out safely and split traffic between revisions
AdvancedDeploy without taking traffic, verify the new revision, then shift traffic gradually and roll back instantly if needed.
How Cloud Run revisions make rollouts reversible
Every Cloud Run deploy creates an immutable revision. Traffic is a separate concern: you can deploy a
revision with --no-traffic, smoke-test it on its revision-specific URL, then split traffic by percentage
between revisions (--to-revisions REV=10) for a canary, and promote to 100% once healthy. Because old
revisions stay around, rollback is just pointing 100% of traffic back at the previous one — instant, no
rebuild. This is the safest production rollout pattern Cloud Run offers.
Canary, promote, and roll back by traffic
# Deploy the new revision but send it no traffic yet
gcloud run deploy aurora-api --image "$IMAGE" --region europe-west1 \
--no-traffic --tag canary
# Send 10% to the canary, keep 90% on the current revision
gcloud run services update-traffic aurora-api --region europe-west1 \
--to-tags canary=10
# Healthy? Promote to 100%. Broken? Roll all traffic to the last good revision.
gcloud run services update-traffic aurora-api --region europe-west1 --to-latest
gcloud run services update-traffic aurora-api --region europe-west1 \
--to-revisions PREVIOUS_REVISION=100Agent prompt — paste into an agent with repo access
Role: Senior SRE in this repo.
Context: aurora-api is on Cloud Run. We want a guarded rollout: deploy with no traffic, health-check the revision URL, canary 10%, then promote — and a one-command rollback.
Task: Write ./rollout.sh and ./rollback.sh implementing this flow with gcloud.
Requirements:
- rollout.sh: deploy `--no-traffic --tag canary`, resolve the canary tagged URL, curl /healthz and abort if not 200, then `update-traffic --to-tags canary=10`. A `--promote` flag runs `update-traffic --to-latest`.
- rollback.sh: shift 100% traffic to the previously-serving revision (resolve it from `gcloud run revisions list`).
- `set -euo pipefail`; no hardcoded revision names.
Tests / acceptance:
- `bash ./rollout.sh` ends with `gcloud run services describe aurora-api --region europe-west1 --format='value(status.traffic)'` showing the canary at 10%.
- `bash ./rollback.sh` restores 100% to the prior revision (verified via the same describe).
Output: a unified diff plus a one-paragraph note on why immutable revisions make rollback instant.Reach for GKE only when Cloud Run runs out
AdvancedKnow the boundary: stay on Cloud Run for stateless HTTP, and move to GKE (managed Kubernetes) only for workloads it can’t serve.
When a managed Kubernetes cluster earns its complexity
Cloud Run handles the common case — a stateless, request-driven container — with almost no operational surface. GKE (Google Kubernetes Engine) is Google’s managed Kubernetes; it earns its added complexity when you genuinely need what Cloud Run doesn’t offer: long-running or non-HTTP workloads, fine-grained pod networking and service meshes, DaemonSets, custom schedulers, GPUs with specific topologies, or operators that expect the full Kubernetes API. The cost is real — you now own cluster upgrades, node pools, and manifests. Default to Cloud Run; graduate to GKE for a specific requirement, not by reflex. GKE Autopilot trims some of that operational load if you do move.
A minimal Autopilot cluster (only if you need it)
# Autopilot manages the nodes for you; pay per pod resource request
gcloud container clusters create-auto aurora-cluster \
--region europe-west1
# Point kubectl at it, then apply your manifests
gcloud container clusters get-credentials aurora-cluster --region europe-west1
kubectl get nodesWhere to take it next
- Put this cloud under a real backend in Aurora Commerce, where Cloud Run fronts a Cloud SQL Postgres database for checkout — the exact connector and IAM path from this track.
- The service you deploy here is built in the Go track; come for the 15 MB container, deploy it with the Step 3 command.
- Adding AI to the same service? The Gemini API pairs naturally — call it from your Cloud Run container with the runtime service account you set up in Step 8.