mcpbeat Sign in

GCP Essentials Agent Skill

Use when running a small product on core Google Cloud via the gcloud CLI: a project, Cloud Run deploys, a locked-down Cloud Storage bucket, managed Cloud SQL, and least-privilege IAM wiring them together. NOT AWS (that is `aws-essentials`), NOT the CI pipeline that ships the image (that is `deployment`), NOT Postgres schema/query tuning (that is `postgresdb`).

8k tokens
context cost
the whole folder, loaded on every use
7
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill gcp-essentials

What comes with it

18 747 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/deploy-recipes.md
references/iam-and-auth.md
references/networking-and-sql.md
scripts/verify.sh

The instruction itself

10 sections, as written by the author

GCP essentials

Get a small product running on the core of Google Cloud — safely and cheaply — with

the gcloud CLI as the source of truth. The console is fine for reading; the CLI is

what you commit, review, and reproduce. Bias toward **secure-by-default and near-zero

bill**, not "every GCP service".

Four primitives carry most products, plus the project/billing scaffold under them:

  • IAM — who can do what. Get this wrong and nothing else matters.
  • Cloud Run — serverless containers, scale to zero.
  • Cloud Storage — object storage (buckets).
  • Cloud SQL — managed Postgres/MySQL.

Out of scope, route elsewhere: AWS -> ../aws-essentials/SKILL.md.

Building/shipping the image itself -> ../docker/SKILL.md

/ ../github-actions/SKILL.md / ../deployment/SKILL.md.

Postgres schema/index/query tuning -> ../postgresdb/SKILL.md.

App-level injection/secret-handling review -> ../secure-coding/SKILL.md.

Logging/alerting/SLOs as a practice -> ../monitoring/SKILL.md.

Backup strategy as a discipline -> ../backups/SKILL.md.

One-click PaaS where you never touch IAM/VPC -> ../vercel/SKILL.md

/ ../railway/SKILL.md / ../render/SKILL.md

/ ../fly-io/SKILL.md.

0. Bootstrap a project

One project per environment (e.g. acme-prod, acme-staging). Projects are the IAM

and billing boundary; mixing prod and dev in one project is how a staging credential

deletes prod data.

# Create the project and point gcloud at it
gcloud projects create acme-prod --name="Acme prod"
gcloud config set project acme-prod
gcloud config set run/region europe-west1   # set once; every run command inherits it

# Link billing (no billing = APIs 403). Find your account id first:
gcloud billing accounts list
gcloud billing projects link acme-prod --billing-account=0X0X0X-0X0X0X-0X0X0X

# Enable ONLY the APIs this product needs. Why: every enabled API widens the
# attack surface and some bill the moment they are on.
gcloud services enable \
  run.googleapis.com \
  sqladmin.googleapis.com \
  storage.googleapis.com \
  secretmanager.googleapis.com \
  iam.googleapis.com

1. IAM without footguns

A binding is member + role on a resource. Members come in three flavours you will

actually type:

| Member type | Syntax | Use for |

|------------------|-------------------------------------|----------------------------------|

| User | user:[email protected] | a human |

| Group | group:[email protected] | a team (manage in Workspace) |

| Service account | serviceAccount:[email protected] | a workload identity |

Grant grammar — bind at the smallest resource that works (project here, but prefer

bucket/instance scope when the role supports it):

gcloud projects add-iam-policy-binding acme-prod \
  --member="serviceAccount:[email protected]" \
  --role="roles/cloudsql.client"

Choosing a role:

| Role kind | Example | When |

|----------------|-------------------------------|-------------------------------------------------------------|

| Primitive | roles/owner, roles/editor | Almost never on a workload — project-wide, far too broad. |

| Predefined | roles/storage.objectAdmin | Default. Google-maintained, scoped to one service. |

| Custom | your own permission list | Only when no predefined role fits — you now own the upkeep. |

Two hard rules, each with teeth:

  • Never run a workload as the default compute service account. It carries

Editor on the whole project, so a single RCE in your container = full project

takeover. Mint a dedicated SA per service and pass it explicitly (see Cloud Run).

   gcloud iam service-accounts create api-sa --display-name="api runtime"
  • Never create service-account JSON keys. A leaked key is a long-lived,

un-rotated credential. Use the *attached* SA on Cloud Run/Compute, and Workload

Identity Federation for external/CI auth (GitHub Actions). If ... keys create is

in your runbook, the runbook is wrong.

The predefined-role catalog, WIF for keyless CI, SA impersonation, IAM Recommender and

Conditions live in references/iam-and-auth.md.

2. Cloud Run

Minimal *safe* deploy: dedicated runtime SA, explicit region, no anonymous ingress.

gcloud run deploy api \
  --image=europe-west1-docker.pkg.dev/acme-prod/app/api:1.4.0 \
  --region=europe-west1 \
  [email protected] \
  --no-allow-unauthenticated
  • --service-account sets the runtime identity. Omit it and the revision runs as the

over-privileged default compute SA — the rule-1 footgun. Always pass it.

  • --no-allow-unauthenticated keeps the service private (callers need

roles/run.invoker). Flip to --allow-unauthenticated *only* for a genuinely public

endpoint. Open by accident and you have shipped an unauthenticated API.

Production knobs:

# Cold starts hurt: pin a warm instance and boost CPU on startup.
# Default min-instances is 0 (scales to zero); default max is 100 (your cost ceiling).
gcloud run services update api --region=europe-west1 \
  --min-instances=1 --cpu-boost --max-instances=20

Config vs secrets — secrets never go in --set-env-vars, because env vars show up

in plaintext in describe, logs and the console. Mount them from Secret Manager:

gcloud run deploy api --region=europe-west1 \
  [email protected] \
  --set-env-vars="LOG_LEVEL=info" \
  --set-secrets="DB_PASSWORD=db-password:latest"

3. Cloud Storage

Create buckets locked down; loosen deliberately, never the reverse.

gcloud storage buckets create gs://acme-prod-uploads \
  --location=europe-west1 \
  --uniform-bucket-level-access \
  --public-access-prevention
  • --uniform-bucket-level-access (UBLA) turns off per-object ACLs so access is *only*

IAM — one place to reason about, one place to audit. There is a 90-day window to

revert UBLA; after that it is permanent, so set it at creation.

  • --public-access-prevention makes a public grant impossible even by mistake.

Grant access to the workload, not the world:

gcloud storage buckets add-iam-policy-binding gs://acme-prod-uploads \
  --member="serviceAccount:[email protected]" \
  --role="roles/storage.objectAdmin"

Need to hand a file to an anonymous browser? Use a signed URL (time-limited),

never allUsers:

gcloud storage sign-url gs://acme-prod-uploads/report.pdf --duration=15m \
  --impersonate-service-account=api-sa@acme-prod.iam.gserviceaccount.com

--impersonate-service-account is not decoration. Signing needs a private key, and the

keyless model this skill mandates (attached SA, no JSON keys) hands you an ADC *token*,

not a key. The flag tells gcloud to sign via the IAM signBlob API instead — so the

caller must hold roles/iam.serviceAccountTokenCreator (which grants

iam.serviceAccounts.signBlob) on api-sa. Without it, the command fails or

silently wants a key file, which would reopen the rule-2 footgun. Grant it once:

gcloud iam service-accounts add-iam-policy-binding \
  [email protected] \
  --member="serviceAccount:[email protected]" \
  --role="roles/iam.serviceAccountTokenCreator"

Durability one-liners:

gcloud storage buckets update gs://acme-prod-uploads --versioning      # keep old versions
gcloud storage buckets update gs://acme-prod-uploads \
  --lifecycle-file=lifecycle.json                                      # auto-expire/age out

4. Cloud SQL

Create a managed Postgres with a private IP and no public IP — the public IP is

the part that gets scanned and brute-forced.

gcloud sql instances create acme-db \
  --database-version=POSTGRES_16 \
  --edition=ENTERPRISE \
  --region=europe-west1 \
  --tier=db-f1-micro \
  --no-assign-ip \
  --network=projects/acme-prod/global/networks/default

--edition=ENTERPRISE is mandatory here, not optional. From POSTGRES_16 up the

default edition is Enterprise *Plus*, which only runs on N2/C4A/N4 machine series — the

shared-core db-f1-micro is an Enterprise-only tier, so the create fails without

this flag. Want the cheapest box? Stay on Enterprise. Reach for Enterprise Plus only

when you actually need its dedicated cores and faster failover, and drop --tier for a

--cpu/--memory pair then.

Put the password in Secret Manager, not in a flag or a file:

gcloud sql users create app --instance=acme-db --password="$(openssl rand -base64 24)"
printf '%s' "$(openssl rand -base64 24)" | \
  gcloud secrets create db-password --data-file=-

Attach the instance to Cloud Run — serverless connects over a Unix socket, **no Auth

Proxy sidecar needed**:

gcloud run deploy api --region=europe-west1 \
  [email protected] \
  --add-cloudsql-instances=acme-prod:europe-west1:acme-db \
  --set-secrets="DB_PASSWORD=db-password:latest"
# In the app, connect via the socket:
#   host=/cloudsql/acme-prod:europe-west1:acme-db

The Cloud SQL Auth Proxy (short-lived certs, TLS 1.3) is for connecting from

*outside* — local dev or a non-serverless host — not for Cloud Run. Direct VPC egress vs

legacy connectors, private IP / PSC, proxy invocation and pooling are in

references/networking-and-sql.md.

5. Wire it together

One service, one dedicated SA, exactly the roles it needs — and nothing else.

# Identity
gcloud iam service-accounts create api-sa --display-name="api runtime"
[email protected]

# Exactly four predefined roles. No Editor, no Owner.
gcloud projects add-iam-policy-binding acme-prod \
  --member="serviceAccount:$SA" --role="roles/cloudsql.client"
gcloud secrets add-iam-policy-binding db-password \
  --member="serviceAccount:$SA" --role="roles/secretmanager.secretAccessor"
gcloud storage buckets add-iam-policy-binding gs://acme-prod-uploads \
  --member="serviceAccount:$SA" --role="roles/storage.objectAdmin"

# Deploy with all three wired in
gcloud run deploy api --region=europe-west1 \
  --image=europe-west1-docker.pkg.dev/acme-prod/app/api:1.4.0 \
  --service-account="$SA" \
  --no-allow-unauthenticated \
  --add-cloudsql-instances=acme-prod:europe-west1:acme-db \
  --set-secrets="DB_PASSWORD=db-password:latest" \
  --set-env-vars="BUCKET=acme-prod-uploads"

Note the scoping: cloudsql.client is project-wide (the role needs it), but the

storage and secret grants are bound to the *specific* bucket and secret, not the

project. Grant narrow.

Copy-paste runbooks for each piece — image build and push, private container deploy,

attach SQL, mount a secret, full teardown — are in

references/deploy-recipes.md.

6. Cost & teardown

  • Cloud Run scales to zero by default — an idle service costs ~nothing. Keep

--min-instances=0 on staging.

  • Cap blast radius with --max-instances and a budget alert (full command in

references/deploy-recipes.md):

  gcloud billing budgets create --billing-account=0X0X0X-0X0X0X-0X0X0X \
    --display-name="acme-prod" --budget-amount=50 \
    --threshold-rule=percent=0.9
  • Tear down in dependency order so nothing dangles:
  gcloud run services delete api --region=europe-west1
  gcloud sql instances delete acme-db
  gcloud storage rm --recursive gs://acme-prod-uploads

Anti-patterns

| Bad | Good | Why |

|-----|------|-----|

| Deploy with no --service-account | Pass a dedicated per-service SA | Default compute SA has Editor; an RCE becomes project takeover |

| gcloud iam service-accounts keys create key.json | Attached SA + Workload Identity Federation | JSON keys are long-lived, leak, and are rarely rotated |

| --role=roles/editor on a workload SA | Scoped predefined roles (cloudsql.client, …) | Primitive roles grant far more than the service needs |

| Bucket public via allUsers | Signed URL via --impersonate-service-account (+ Token Creator) | A public bucket is a data leak; keyless signing needs signBlob, not a key file |

| Bucket created without UBLA/PAP | --uniform-bucket-level-access --public-access-prevention at create | ACLs sprawl; PAP blocks accidental public grants |

| Cloud SQL with public IP open to 0.0.0.0/0 | --no-assign-ip + private IP / Auth Proxy | Public DB IPs get scanned and brute-forced |

| Secrets in --set-env-vars | --set-secrets from Secret Manager | Env vars are plaintext in describe, logs, console |

| gcloud services enable everything | Enable only the APIs you use | Each API widens attack surface; some bill on enable |

| No --min-instances on prod, then blame cold starts | --min-instances=1 --cpu-boost on prod | Scale-to-zero is the cause; pin a warm instance |

| Auth Proxy sidecar on Cloud Run | --add-cloudsql-instances + /cloudsql/... socket | Serverless connects natively; the proxy is for outside-VPC |

Verify

scripts/verify.sh is an offline static linter (no GCP calls, no network) over files

that contain gcloud command blocks. It flags the unsafe patterns above: JSON key

creation, roles/owner|roles/editor bound to a service account, bucket creates missing

UBLA/PAP, Cloud SQL public IP without private IP, and Cloud Run deploys missing

--service-account.

bash scripts/verify.sh path/to/runbook.sh        # one file
bash scripts/verify.sh path/to/dir/              # recurse a directory

It prints PASS/FAIL per check and exits nonzero on any FAIL. An empty or

clean target passes (exit 0).

How to use it

Copy the folder

Take ericrisco/gcp-essentials from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.