Interactive GCP Cloud Build + Cloud Run setup. Provisions APIs, AR, SAs, IAM, secrets, and triggers.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add MatiasComercio/agentic-config --skill gcp-setup --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Gcp Setup?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/matiascomercio-gcp-setup)More formats (shields.io, HTML) on the badges page.
---
name: gcp-setup
description: "Interactive GCP Cloud Build + Cloud Run setup. Provisions APIs, AR, SAs, IAM, secrets, and triggers."
project-agnostic: true
allowed-tools:
- Bash
- Read
- Write
- Glob
- Grep
- AskUserQuestion
- mcp__voicemode__converse
---
# GCP Setup — Cloud Build + Cloud Run
Interactive skill for bootstrapping GCP infrastructure for Cloud Build + Cloud Run projects. Handles everything from `gcloud auth login` to verified production deploy.
## Security First
Every phase validates security posture. The skill enforces:
- Dedicated runtime service account (never default compute SA)
- Per-secret IAM bindings (never project-level secretAccessor)
- Approval-gated Cloud Build triggers
- Non-root container execution
- Optional app-level OAuth with configurable domain restriction
- Same-origin CORS in production
- Secrets in Secret Manager only (never env vars, never in images)
## Tool Paths
All tools are relative to this skill directory:
```
SKILL_DIR="."
```
## Workflow
### Phase 0: Config Detection
Detect and load configuration before any infrastructure operations.
#### 0a: User-Level Customization (BLOCKING)
Check `~/.agents/customization/gcp-setup/index.md` for saved preferences.
If missing, use AskUserQuestion to collect:
- **Company domain** (e.g., `example.com`)
- **Company name** (e.g., `Example Corp`)
- **GitHub organization** (e.g., `ExampleOrg`)
- **Preferred GCP region** (e.g., `us-central1`)
Save to `~/.agents/customization/gcp-setup/index.md`:
```markdown
# GCP Setup Preferences
- domain: example.com
- company: Example Corp
- github_org: ExampleOrg
- region: us-central1
```
If values already exist, present them as defaults. If user provides different values, update `index.md`.
#### 0b: Project-Level Config (BLOCKING)
Detect `.gcp-setup.yml` in the project root.
**If exists:** Load and present current config to user. AskUserQuestion: "Config found. Proceed with these settings, or modify?"
**If missing:** Auto-detect everything possible, then confirm. Typeform-style: one question at a time, smart defaults, minimal friction.
**Step 1: Silent auto-detection** (no user interaction — run all probes first)
```bash
# GitHub repo from git remote
GITHUB_REMOTE=$(git remote get-url origin || true)
GITHUB_SLUG=$(printf "%s\n" "$GITHUB_REMOTE" | sed -E 's#.*[:/]([^/]+/[^/.]+)(\.git)?$#\1#')
DIR_NAME=$(basename "$(pwd)")
# Runtime + framework
RUNTIME=""
[[ -f package.json ]] && RUNTIME="nodejs"
[[ -f requirements.txt || -f pyproject.toml ]] && RUNTIME="python"
[[ -f go.mod ]] && RUNTIME="go"
FRAMEWORK=""
[[ -f package.json ]] && grep -q '"express"' package.json && FRAMEWORK="express"
for file in requirements.txt pyproject.toml; do
[[ -f "$file" ]] && grep -q 'fastapi' "$file" && FRAMEWORK="fastapi"
done
# Feature detection from code/dependencies
HAS_OAUTH=$(grep -rl -m 1 "passport\|GoogleStrategy\|google-auth-library\|authlib" \
--include="*.ts" --include="*.js" --include="*.py" \
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor . | head -1)
HAS_FIRESTORE=$(grep -rl -m 1 "@google-cloud/firestore\|google.cloud.firestore" \
--include="*.ts" --include="*.js" --include="*.py" --include="*.go" \
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor . | head -1)
# Build configs, Dockerfile, health endpoint
CLOUDBUILD_FILES=$(compgen -G "cloudbuild*.yaml"; compgen -G "cloudbuild*.yml")
DOCKERFILE=$(find . -maxdepth 2 -name Dockerfile -print | head -1)
HEALTH_EP=$(grep -roh '"/health[z]*"\|"/api/health"' \
--include="*.ts" --include="*.js" --include="*.py" . | head -1 | tr -d '"')
# Secret manifest from .env.example
ENV_EXAMPLE=""
for candidate in .env.example .env.sample env.example; do
[[ -f "$candidate" ]] && ENV_EXAMPLE="$candidate" && break
done
# GCP projects (for matching)
if GCLOUD_BIN=$(command -v gcloud); then
GCP_PROJECTS=$("$GCLOUD_BIN" projects list --format="value(projectId)")
else
GCP_PROJECTS=""
fi
```
Load user preferences from Phase 0a (`~/.agents/customization/gcp-setup/index.md`): domain, company, org, region.
**Step 2: Typeform questionnaire** — one AskUserQuestion per step
Rules:
- First option = recommended default (auto-detected or convention-based)
- Skip question entirely if auto-detection is unambiguous (e.g., GitHub repo from remote)
- NEVER ask about convention-derived values — fill them automatically
- Options must be concrete and actionable — no "Chat about this" or "Need to setup later"
- Target: 2-4 questions max for a typical project
**Q1: GCP Projects** (required — cannot auto-detect)
Filter `$GCP_PROJECTS` for pairs matching `*-stage/*-dev` + `*-prod` patterns, or containing `$DIR_NAME`. Present best-match pair first:
1. `detected-app-dev` + `detected-app-prod` (if pair found)
2. Pick from my GCP projects (show filtered short-list)
3. Enter project IDs manually
> **Multi-app pattern:** Multiple apps can share the same GCP projects. Each app gets its own `.gcp-setup.yml` with unique `sa_prefix`, `service_name`, and secret names. Shared resources (AR repo, KMS keyring, GitHub connection) are created idempotently — the second app's provisioning simply finds them already in place. See `cookbook/environments.md` for naming conventions and examples.
**Q2: Service Names** (skip if `$DIR_NAME` is clean)
1. `${DIR_NAME}-stage`, `${DIR_NAME}` — convention: stage suffixed, prod is base name
2. Enter custom names
If `$DIR_NAME` produces clean service names (lowercase, no special chars, <30 chars), skip this question and use option 1 automatically — show in review.
**Q3: Detected Features** (confirmation only)
Present auto-detected features as a summary:
- Runtime: `$RUNTIME` / `$FRAMEWORK` (or "not detected")
- OAuth: Yes/No (based on `$HAS_OAUTH`)
- Firestore: Yes/No (based on `$HAS_FIRESTORE`)
- Health endpoint: `$HEALTH_EP` or `/api/health` (default)
1. Confirm
2. Change (let user toggle specific features)
If nothing ambiguous was detected, skip this question too.
**Q3b: OAuth Mode** (conditional — only when OAuth = Yes from Q3)
> **How should this app handle authentication?**
>
> 1. **Standalone** — This app authenticates users directly via Google OAuth.
> Each app needs its own redirect URIs registered in the GCP Console.
> Standard setup for single apps or small teams.
>
> 2. **Auth-proxy** — This app IS a centralized authentication service.
> Register its OAuth redirect URIs once. Future apps delegate auth to
> this proxy — no additional Console configuration needed per app.
> See `cookbook/auth-proxy.md` for full architecture.
>
> 3. **IAP** (recommended for internal tools) — Google Cloud Identity-Aware Proxy.
> Google manages the entire auth flow. Enable per service with one CLI flag
> (`--iap`). No redirect URIs, no OAuth clients to manage per app, no extra
> infrastructure. Access controlled via IAM policies.
> Trade-off: no custom login UI (Google's IAP login page).
> See `cookbook/iap-setup.md` for setup guide.
Cannot be auto-detected — always ask when OAuth is enabled.
Set `oauth.mode` to `standalone` (option 1), `auth-proxy` (option 2), or `iap` (option 3).
**Q4: Review & Generate**
Build the COMPLETE config YAML from auto-detected values + user answers + conventions. Present it formatted.
Convention-derived values (NEVER ask the user for these):
- `artifact_registry.repo_name`: `docker-images`
- `sa_prefix.runtime`: `app-runtime`
- `sa_prefix.cloudbuild`: `app-cloudbuild`
- `kms.keyring`: `cloudbuild-keys`
- `kms.key`: `connection-key`
- `github.connection_name`: repo name from slug
- `github.linked_repo`: slug with `/` replaced by `-`
- `oauth.callback_path`: `/auth/google/cb` (Express + Passport) or `/api/auth/callback/google` (Auth.js / NextAuth) — register BOTH in OAuth client redirect URIs
- `oauth.domain_restriction`: company domain from user preferences
- `oauth.consent_type`: `Internal`
- `oauth.mode`: from Q3b answer (default: `standalone`). When `iap` is selected, `oauth.callback_path` and `oauth.consent_type` are not used (IAP manages these).
- `firestore.collection`: `express-sessions`
- `firestore.ttl_field`: `expiresAt`
- Build configs: detected files or `cloudbuild-stage.yaml` / `cloudbuild-prod.yaml`
- Secrets: OAuth secrets if OAuth detected + `session-secret` with `auto_generate: true`
1. Save as `.gcp-setup.yml`
2. Modify specific values (re-present after changes)
Generate `.gcp-setup.yml` using `assets/gcp-setup-example.yml` as template reference. Write to project root.
> `.gcp-setup.yml` contains no secrets and should be committed to version control.
**After saving config, ensure `.env` files are gitignored:**
```bash
# Ensure .env files are gitignored (secret values must never be committed)
if [[ -f .gitignore ]]; then
grep -qxF '.env' .gitignore || echo '.env' >> .gitignore
grep -qxF '.env.*' .gitignore || echo '.env.*' >> .gitignore
else
printf '.env\n.env.*\n' > .gitignore
fi
```
#### 0c: Build Files Generation
After config is saved, check for missing build files and generate from templates. See `cookbook/build-setup.md` for the full non-tech guide.
**Detection** (uses variables from Phase 0b Step 1):
```bash
HAS_DOCKERFILE=$([[ -n "$DOCKERFILE" ]] && echo true || echo false)
HAS_DOCKERIGNORE=$([[ -f .dockerignore ]] && echo true || echo false)
HAS_CLOUDBUILD=$([[ -n "$CLOUDBUILD_FILES" ]] && echo true || echo false)
```
**1. `.dockerignore`** (always generate if missing):
```bash
cp "$SKILL_DIR/assets/dockerignore-template" .dockerignore
```
**2. `Dockerfile`** (generate if missing, based on detected `$RUNTIME`):
| Runtime | Template | Default CMD |
|---------|----------|-------------|
| `nodejs` | `$SKILL_DIR/assets/dockerfile-node.template` | `CMD ["node", "server.js"]` |
| `python` | `$SKILL_DIR/assets/dockerfile-python.template` | `CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]` |
| `go` | `$SKILL_DIR/assets/dockerfile-go.template` | `ENTRYPOINT ["/app/server"]` |
After copying the template, adjust the `CMD`/`ENTRYPOINT` based on detected framework and entry point:
- Express: detect `main` field in `package.json` or `server.js`/`index.js`/`app.js`
- Next.js: `CMD ["npm", "start"]`
- FastAPI: detect `main:app` or `app.main:app` from imports
- Flask/Django: detect `app:app` or `project.wsgi:application`
If runtime is not detected, use AskUserQuestion to ask which runtime to use.
**3. CloudBuild YAMLs** (generate if missing):
Read `$SKILL_DIR/assets/cloudbuild-template.yaml` and substitute tokens from config. Generate TWO files:
**`cloudbuild-stage.yaml`:**
```bash
CONFIG=".gcp-setup.yml"
PROJECT_ID=$(yq -r '.projects.stage.id' "$CONFIG")
SERVICE_NAME=$(yq -r '.projects.stage.service_name' "$CONFIG")
REGION=$(yq -r '.region' "$CONFIG")
REPO_NAME=$(yq -r '.artifact_registry.repo_name' "$CONFIG")
IMAGE_NAME="$SERVICE_NAME"
MAX_INSTANCES=$(yq -r '.app.max_instances_stage // 5' "$CONFIG")
SA_PREFIX=$(yq -r '.sa_prefix.runtime' "$CONFIG")
RUNTIME_SA="${SA_PREFIX}@${PROJECT_ID}.iam.gserviceaccount.com"
APP_DIR=$(dirname "$(yq -r '.app.dockerfile_path // "Dockerfile"' "$CONFIG")")
[[ "$APP_DIR" == "." || -z "$APP_DIR" ]] && APP_DIR="."
# Resolve auth flag and IAP settings based on oauth.mode
OAUTH_MODE=$(yq -r '.oauth.mode // "standalone"' "$CONFIG")
if [[ "$OAUTH_MODE" == "iap" ]]; then
_AUTH_FLAG="--no-allow-unauthenticated"
_IAP_FLAG="--iap"
_ENTRYPOINT="gcloud beta"
else
_AUTH_FLAG="--allow-unauthenticated"
_IAP_FLAG=""
_ENTRYPOINT="gcloud"
fi
# Generate secrets mapping from manifest
SECRETS_MAPPING=""
SECRET_COUNT=$(yq -r '.secrets | length' "$CONFIG")
for ((i=0; i<SECRET_COUNT; i++)); do
NAME=$(yq -r ".secrets[$i].name" "$CONFIG")
ENV_VAR=$(yq -r ".secrets[$i].env_var" "$CONFIG")
[[ -n "$SECRETS_MAPPING" ]] && SECRETS_MAPPING="${SECRETS_MAPPING},"
SECRETS_MAPPING="${SECRETS_MAPPING}${ENV_VAR}=${NAME}:latest"
done
```
Substitute all `__TOKEN__` placeholders in the template and write to `cloudbuild-stage.yaml`:
- Replace `__ENTRYPOINT__` with `$_ENTRYPOINT` (i.e., `gcloud beta` for IAP, `gcloud` otherwise)
- Replace `__AUTH_FLAG__` with `$_AUTH_FLAG`
- If `$_IAP_FLAG` is non-empty, replace `__IAP_FLAG__` with `--iap`
- If `$_IAP_FLAG` is empty, **remove the entire `- '__IAP_FLAG__'` line** from the generated YAML (do not leave an empty string arg)
- If `$SECRETS_MAPPING` is empty, **remove the entire `--set-secrets=__SECRETS_MAPPING__` line** (an empty `--set-secrets=` causes deploy failure)
- If `$ENV_VARS` is empty, **remove the entire `--set-env-vars=__ENV_VARS__` line** (an empty `--set-env-vars=` causes deploy failure)
**`cloudbuild-prod.yaml`:** Same process with prod values and `max_instances_prod` (default: `100`).
**AskUserQuestion gate:** Present a summary of generated files:
```
Generated build files:
- .dockerignore (universal exclusions)
- Dockerfile (Node.js multi-stage, non-root)
- cloudbuild-stage.yaml (stage project, max 5 instances)
- cloudbuild-prod.yaml (prod project, max 100 instances)
Review and confirm, or modify specific files.
```
All generated files should be committed to version control.
#### 0d: Project Documentation
After build files are in place, generate project documentation if missing. These docs help onboard new team members and guide AI assistants.
**Detection:**
```bash
HAS_SECRETS_GUIDE=$([[ -f docs/managing-secrets-and-envs.md ]] && echo true || echo false)
HAS_README=$([[ -f README.md ]] && echo true || echo false)
HAS_AGENTS=$([[ -f AGENTS.md || -f CLAUDE.md ]] && echo true || echo false)
```
**Compute deterministic URLs** (needed for templates):
```bash
CONFIG=".gcp-setup.yml"
STAGE_PROJECT=$(yq -r '.projects.stage.id' "$CONFIG")
PROD_PROJECT=$(yq -r '.projects.prod.id' "$CONFIG")
STAGE_SERVICE=$(yq -r '.projects.stage.service_name' "$CONFIG")
PROD_SERVICE=$(yq -r '.projects.prod.service_name' "$CONFIG")
REGION=$(yq -r '.region' "$CONFIG")
STAGE_NUM=$(gcloud projects describe "$STAGE_PROJECT" --format="value(projectNumber)")
PROD_NUM=$(gcloud projects describe "$PROD_PROJECT" --format="value(projectNumber)")
STAGE_URL="https://${STAGE_SERVICE}-${STAGE_NUM}.${REGION}.run.app"
PROD_URL="https://${PROD_SERVICE}-${PROD_NUM}.${REGION}.run.app"
CB_CONSOLE_STAGE="https://console.cloud.google.com/cloud-build/builds;region=${REGION}?project=${STAGE_PROJECT}"
CB_CONSOLE_PROD="https://console.cloud.google.com/cloud-build/builds;region=${REGION}?project=${PROD_PROJECT}"
```
**For each missing doc, generate from template:**
1. **`docs/managing-secrets-and-envs.md`** — Read `$SKILL_DIR/assets/managing-secrets-and-envs.template.md`, substitute tokens from config. Generate `${SECRETS_TABLE}` from secrets manifest:
```
| Secret Name | Env Var | Environments |
|-------------|---------|--------------|
| oauth-client-id | GOOGLE_CLIENT_ID | stage, prod |
...
```
Generate `${ENV_VARS_TABLE}` for non-secret env vars (`AUTH_URL`, `AUTH_TRUST_HOST`, etc.).
Create `docs/` directory if needed.
2. **`README.md`** — Read `$SKILL_DIR/assets/readme.template.md`, substitute tokens. Set `${PROJECT_NAME}` from directory name (title-cased). Set `${PROJECT_DESCRIPTION}` from a brief AskUserQuestion or auto-detect from `package.json` description. Compute `${HEALTH_ENDPOINT}` and `${HEALTH_RESPONSE}` from config.
3. **`AGENTS.md`** — Read `$SKILL_DIR/assets/agents.template.md`, substitute tokens. Framework-specific values:
| Token | Node.js/Express | Node.js/Next.js | Python/FastAPI | Go |
|-------|----------------|-----------------|----------------|-----|
| `${RUNTIME_DISPLAY}` | Node.js 22 (Alpine in Docker) | Node.js 22 (Alpine in Docker) | Python 3.12 (slim in Docker) | Go 1.23 (distroless in Docker) |
| `${LANGUAGE}` | JavaScript/TypeScript | TypeScript (strict mode) | Python | Go |
| `${PACKAGE_MANAGER}` | npm | npm | pip | go modules |
| `${BUILD_CMD}` | `npm run build` | `npm run build` | N/A | `go build` |
| `${DEV_CMD}` | `npm run dev` | `npm run dev` | `uvicorn main:app --reload` | `go run .` |
| `${LINT_CMD}` | `npm run lint` | `npm run lint` | `ruff check` | `golangci-lint run` |
| `${TYPECHECK_CMD}` | `npx tsc --noEmit` | `npx tsc --noEmit` | `pyright` | `go vet ./...` |
`${STYLE_CONVENTIONS}` — framework-specific conventions (App Router for Next.js, route structure for Express, etc.).
`${AUTH_DESCRIPTION}` — derived from oauth config (e.g., "Google OAuth with `@example.com` domain restriction (Auth.js v5)").
**AskUserQuestion gate:** Present generated docs summary:
```
Generated project documentation:
- docs/managing-secrets-and-envs.md (secrets guide with current inventory)
- README.md (environments, local dev, deploy workflow)
- AGENTS.md (AI assistant project guidelines)
Review and confirm, or modify specific files.
```
All generated docs should be committed to version control.
#### 0e: GCP State Probe
Probe existing infrastructure state (unchanged logic, reads from config):
```bash
CONFIG=".gcp-setup.yml"
STAGE=$(yq -r '.projects.stage.id' "$CONFIG")
PROD=$(yq -r '.projects.prod.id' "$CONFIG")
# Quick check — do APIs exist?
gcloud services list --enabled --filter="name:run.googleapis.com" --project="$STAGE" --format="value(name)"
```
Present state table to user and use AskUserQuestion:
- "Here's what already exists. Which phases do you want to run? (0=all from scratch, 1=preflight only, 2=provision, 3=secrets, 4=oauth, 5=verify)"
- If everything exists: suggest Phase 5 (verify) only
### Phase 1: Preflight Validation
```bash
bash "$SKILL_DIR/tools/preflight.sh" --config .gcp-setup.yml
```
**AskUserQuestion gate:** If any required check fails, present install instructions and wait for confirmation before proceeding.
### Phase 2: GCP Resource Provisioning
**MANDATORY — CMEK Encryption Key Setup:**
Before creating the GitHub connection, provision a Cloud KMS encryption key. Connection data (including GitHub OAuth tokens stored in Secret Manager) MUST be encrypted with a customer-managed key.
```bash
bash "$SKILL_DIR/tools/provision.sh" --config .gcp-setup.yml --cmek-only --env all
```
This enables the Cloud KMS API, creates the keyring + key, provisions the Secret Manager service agent, and grants it `cryptoKeyEncrypterDecrypter` on the key.
**MANUAL GATE — 2nd-gen GitHub Connection:**
After CMEK setup, the user must create GitHub connections in both GCP projects. This CANNOT be automated (requires browser OAuth).
Use AskUserQuestion: "CMEK keys are provisioned. Have you created the 2nd-gen GitHub connection in Cloud Build for both projects? You MUST select the encryption key during connection creation."
If NO, read and present the guide:
```bash
cat "$SKILL_DIR/cookbook/oauth-setup.md"
```
Present the Console URLs (read project IDs from config):
```bash
STAGE=$(yq -r '.projects.stage.id' .gcp-setup.yml)
PROD=$(yq -r '.projects.prod.id' .gcp-setup.yml)
echo "Stage: https://console.cloud.google.com/cloud-build/repositories/2nd-gen?project=$STAGE"
echo "Prod: https://console.cloud.google.com/cloud-build/repositories/2nd-gen?project=$PROD"
```
Steps:
1. Open Console URL
2. Click "Create Host Connection" -> GitHub
3. Set **Region** from config
4. Set **Name** from `github.connection_name`
5. **Encryption:** Select the KMS key — do NOT skip this
6. Click **Connect** -> Authorize Cloud Build GitHub App
7. Link the repository
> **SECURITY WARNING:** Do NOT skip the encryption key selection. Google-managed encryption is NOT acceptable. The connection stores GitHub OAuth tokens in Secret Manager — these must be encrypted with the customer-managed key for key rotation control and compliance.
Wait for user confirmation.
**Run provision:**
```bash
bash "$SKILL_DIR/tools/provision.sh" --config .gcp-setup.yml --env all
```
> **Note:** The temporary `secretmanager.admin` grant auto-expires in 20 minutes. No manual revocation needed.
### Phase 3: Secrets Setup
**Create secret shells:**
```bash
bash "$SKILL_DIR/tools/secrets.sh" --config .gcp-setup.yml --action create --env all
```
**AskUserQuestion:** "How do you want to populate secrets?"
- Option A: From an existing .env file -> `--action populate --env-file /path/to/.env`
- Option B: Interactively (guided prompts)
**Populate secrets:**
```bash
bash "$SKILL_DIR/tools/secrets.sh" --config .gcp-setup.yml \
--action populate --env "$TARGET_ENV" --env-file "$ENV_FILE"
```
**SECURITY WARNING:** If populating both envs from the same .env, warn:
> "OAuth credentials SHOULD differ per environment. Consider running `--env stage` first, then `--env prod --env-file /path/to/prod.env`."
### Phase 4: Authentication Configuration (Conditional)
**Skip entirely when `oauth.enabled` is `false` in config.**
Read `oauth.mode` from config (default: `standalone`).
---
#### Mode: IAP (recommended for internal tools)
When `oauth.mode` is `iap`, authentication is handled entirely by Google Cloud Identity-Aware Proxy. No OAuth clients, no redirect URIs, no custom auth code needed.
**One-time project setup** (first time enabling IAP in this GCP project):
1. **Enable required APIs:**
```bash
for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
gcloud services enable iap.googleapis.com cloudresourcemanager.googleapis.com --project="$PROJECT"
done
```
2. **Configure Google Auth Platform** (Console — one time per project):
- Go to **Google Auth Platform > Branding** in GCP Console
- Set app name, support email, authorized domain
- Go to **Audience** and set to configured consent type (default: **Internal**)
- Go to **Data Access** and add scopes: `openid`, `email`, `profile`
3. **AskUserQuestion gate:**
- "Have you configured Google Auth Platform (Branding + Audience) in the GCP Console? This is a one-time step per project."
**Per-service setup** (automated — repeat for each app):
4. **Enable IAP on Cloud Run services:**
```bash
for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
ENV_KEY=$( [ "$PROJECT" = "$STAGE_PROJECT" ] && echo stage || echo prod )
SERVICE_NAME=$(yq -r ".projects.${ENV_KEY}.service_name" "$CONFIG")
# For existing services:
gcloud beta run services update "$SERVICE_NAME" \
--region="$REGION" \
--iap \
--project="$PROJECT"
# For new deploys, use: gcloud beta run deploy ... --iap --no-allow-unauthenticated
done
```
> **Note:** `services update --iap` works on already-deployed services. For first deploys, use `gcloud beta run deploy ... --iap --no-allow-unauthenticated` or add these flags to the CloudBuild deploy step.
5. **Grant IAP service agent invoker permission:**
```bash
for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
ENV_KEY=$( [ "$PROJECT" = "$STAGE_PROJECT" ] && echo stage || echo prod )
SERVICE_NAME=$(yq -r ".projects.${ENV_KEY}.service_name" "$CONFIG")
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT" --format='value(projectNumber)')
gcloud run services add-iam-policy-binding "$SERVICE_NAME" \
--region="$REGION" \
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-iap.iam.gserviceaccount.com" \
--role="roles/run.invoker" \
--project="$PROJECT"
done
```
6. **Remove public access** (if the service was previously public):
```bash
for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
ENV_KEY=$( [ "$PROJECT" = "$STAGE_PROJECT" ] && echo stage || echo prod )
SERVICE_NAME=$(yq -r ".projects.${ENV_KEY}.service_name" "$CONFIG")
gcloud run services remove-iam-policy-binding "$SERVICE_NAME" \
--region="$REGION" \
--member="allUsers" \
--role="roles/run.invoker" \
--project="$PROJECT" || true
done
```
> **Warning:** If `allUsers` has `roles/run.invoker`, anyone can bypass IAP by calling the Cloud Run URL directly. Always remove public access when enabling IAP.
7. **Grant domain access:**
```bash
DOMAIN=$(yq -r '.oauth.domain_restriction' "$CONFIG")
for PROJECT in "$STAGE_PROJECT" "$PROD_PROJECT"; do
ENV_KEY=$( [ "$PROJECT" = "$STAGE_PROJECT" ] && echo stage || echo prod )
SERVICE_NAME=$(yq -r ".projects.${ENV_KEY}.service_name" "$CONFIG")
gcloud beta iap web add-iam-policy-binding \
--resource-type=cloud-run \
--service="$SERVICE_NAME" \
--region="$REGION" \
--member="domain:${DOMAIN}" \
--role="roles/iap.httpsResourceAccessor" \
--project="$PROJECT"
done
```
8. **Read the IAP cookbook:**
```bash
cat "$SKILL_DIR/cookbook/iap-setup.md"
```
9. **Present IAP guidance** to the user:
- IAP is now active. Users visiting the app will see Google's IAP login page.
- The app receives user identity via headers: `X-Goog-Authenticated-User-Email` and `X-Goog-IAP-JWT-Assertion`.
- Future apps: just add `--iap --no-allow-unauthenticated` to the deploy command and run the IAM bindings above. No Console changes needed.
- See `cookbook/iap-setup.md` for header verification, service-to-service auth, and local development.
> **Note:** When `oauth.mode` is `iap`, OAuth secrets (`oauth-client-id`, `oauth-client-secret`) are NOT needed in the secrets manifest. Google manages the OAuth client internally. Only `session-secret` is needed if the app manages its own sessions.
---
#### Mode: Standalone (default)
When `oauth.mode` is `standalone` (or not set):
**SECURITY WARNING — Display this to the user before proceeding:**
> **The OAuth consent screen MUST be set to the configured consent type (default: "Internal").** If set to "Internal", this restricts authentication to the configured domain only. Do NOT select "External" unless required — doing so would allow ANY Google account to reach the OAuth flow. If you are unsure, stop and consult the team.
This sub-phase is entirely manual. The skill provides exact step-by-step guidance.
1. **Compute deterministic Cloud Run URLs:**
```bash
CONFIG=".gcp-setup.yml"
STAGE=$(yq -r '.projects.stage.id' "$CONFIG")
PROD=$(yq -r '.projects.prod.id' "$CONFIG")
STAGE_SVC=$(yq -r '.projects.stage.service_name' "$CONFIG")
PROD_SVC=$(yq -r '.projects.prod.service_name' "$CONFIG")
REGION=$(yq -r '.region' "$CONFIG")
STAGE_NUM=$(gcloud projects describe "$STAGE" --format="value(projectNumber)")
PROD_NUM=$(gcloud projects describe "$PROD" --format="value(projectNumber)")
echo "Stage URL: https://${STAGE_SVC}-${STAGE_NUM}.${REGION}.run.app"
echo "Prod URL: https://${PROD_SVC}-${PROD_NUM}.${REGION}.run.app"
```
> **Warning:** Do NOT use `status.url` to get the service URL for OAuth configuration. It returns a legacy hash-based URL that differs from the deterministic format. Always compute deterministic URLs.
2. **Read the full OAuth guide:**
```bash
cat "$SKILL_DIR/cookbook/oauth-setup.md"
```
3. **Present step-by-step** with pre-computed URLs:
- Configure Google Auth Platform: Branding, Audience, Data Access
- Create per-environment OAuth 2.0 Client IDs
- Register callback URLs: `https://<cloud-run-url><callback_path from config>`
- Add authorized JavaScript origins
4. **AskUserQuestion gates:**
- "Have you configured Google Auth Platform (Audience set to configured consent type) in the GCP project?"
- "Have you created OAuth 2.0 Client IDs for each environment?"
- "Have you populated the oauth secrets via Phase 3?"
---
#### Mode: Auth-Proxy (Additional Steps)
**When `oauth.mode` is `auth-proxy`**, first complete ALL standalone steps above (the proxy itself needs OAuth registration), then:
5. **Read the auth-proxy cookbook:**
```bash
cat "$SKILL_DIR/cookbook/auth-proxy.md"
```
6. **Present auth-proxy guidance** to the user:
- This app is the centralized auth service. The OAuth registration just completed is the ONLY registration needed.
- Future apps that need authentication will delegate to this proxy instead of registering with Google directly.
- The proxy must implement: Google OAuth flow, JWT signing with a shared secret, `return_url` allowlist validation, domain restriction.
- Client apps must implement: redirect to proxy, JWT verification, session creation.
- Share the proxy URL with the team: `https://<proxy-service>-<project-num>.<region>.run.app`
- See `cookbook/auth-proxy.md` for the complete architecture, implementation checklist, and client integration guide.
7. **AskUserQuestion gate:**
- "Have you reviewed the auth-proxy architecture in `cookbook/auth-proxy.md`? This app will serve as the centralized auth service for future apps."
### Phase 5: Verification
**Run full verification:**
```bash
bash "$SKILL_DIR/tools/verify.sh" --config .gcp-setup.yml --env all
```
**Interpret results:**
- All PASS -> Setup complete. Present next steps (first deploy, PR workflow).
- Any FAIL -> Present remediation commands from verify.sh output.
- Re-run verify after remediation.
### Post-Setup: First Deploy
After all phases pass, guide the user through the first deploy:
1. Push a branch and open a PR
2. Comment `/gcbrun` on the PR to trigger stage build
3. Approve the build in Cloud Build console
4. Wait for build + deploy
5. Verify health check using configured endpoint
6. If OAuth enabled: test authentication with configured domain account
7. Merge PR to main for production deploy
### Troubleshooting
If issues arise post-deploy, run diagnostics:
```bash
bash "$SKILL_DIR/tools/diagnose.sh" --config .gcp-setup.yml --env "$TARGET_ENV"
```
For detailed troubleshooting reference:
```bash
cat "$SKILL_DIR/cookbook/troubleshooting.md"
```
## CI Setup: PR Template
To add the deploy checklist to PRs:
```bash
cat "$SKILL_DIR/assets/pr-template.md"
```
Copy this to `.github/PULL_REQUEST_TEMPLATE.md`, replacing `${STAGE_URL}` and `${CB_CONSOLE_URL}` with actual values.
## Reference
| Resource | Location |
|----------|----------|
| Build setup guide | `cookbook/build-setup.md` |
| Prerequisites | `cookbook/prerequisites.md` |
| Security model | `cookbook/security-model.md` |
| OAuth setup guide | `cookbook/oauth-setup.md` |
| Auth.js on Cloud Run | `cookbook/authjs-setup.md` |
| Auth-proxy guide | `cookbook/auth-proxy.md` |
| IAP setup guide | `cookbook/iap-setup.md` |
| Troubleshooting | `cookbook/troubleshooting.md` |
| Environment configs | `cookbook/environments.md` |
| Cloud Build template | `assets/cloudbuild-template.yaml` |
| PR template | `assets/pr-template.md` |
| .env template | `assets/env-example` |
| Secrets guide template | `assets/managing-secrets-and-envs.template.md` |
| README template | `assets/readme.template.md` |
| AGENTS.md template | `assets/agents.template.md` |
| Config example | `assets/gcp-setup-example.yml` |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!