|
| 1 | +# PR 4 Stage 1 Staging Cloud Run Service Runbook |
| 2 | + |
| 3 | +Stage 1 of the public host cutover splits staging Cloud Run deploys onto a dedicated |
| 4 | +`policyengine-api-staging` service. Previously, both the staging and production CI tracks |
| 5 | +targeted the single `policyengine-api` service, and the staging promote step put a |
| 6 | +staging-configured revision on 100% of the production service URL during every push. That |
| 7 | +is harmless while nothing public routes to the service URL, and unacceptable once a load |
| 8 | +balancer fronts it. |
| 9 | + |
| 10 | +## One-Time Bootstrap (completed 2026-07-02) |
| 11 | + |
| 12 | +The CI pipeline cannot create this service itself, for two reasons: |
| 13 | + |
| 14 | +1. `deploy_cloud_run_candidate.sh` always deploys with `--no-traffic` (the PR 3 |
| 15 | + candidate-then-promote pattern). gcloud rejects `--no-traffic` when creating a new |
| 16 | + service, because a new service must route 100% of traffic to its first revision. |
| 17 | +2. The GitHub deploy service account holds `roles/run.developer`, which can create and |
| 18 | + deploy services but cannot set IAM policy. The `allUsers` -> `roles/run.invoker` |
| 19 | + binding that `--allow-unauthenticated` needs on a new service must be created by a |
| 20 | + project owner or `roles/run.admin` principal. (The production service's binding was |
| 21 | + created manually during PR 3.) |
| 22 | + |
| 23 | +Bootstrap command, run once by a project owner before merging the Stage 1 PR: |
| 24 | + |
| 25 | +```bash |
| 26 | +gcloud run deploy policyengine-api-staging \ |
| 27 | + --project policyengine-api \ |
| 28 | + --region us-central1 \ |
| 29 | + --image us-docker.pkg.dev/cloudrun/container/hello \ |
| 30 | + --allow-unauthenticated \ |
| 31 | + --service-account policyengine-api-cr-runtime@policyengine-api.iam.gserviceaccount.com |
| 32 | +``` |
| 33 | + |
| 34 | +The placeholder image is intentional: the first CI staging deploy replaces the revision |
| 35 | +entirely, since `deploy_cloud_run_candidate.sh` sets every flag (image, env vars, secrets, |
| 36 | +resources, service account, Cloud SQL attachment) explicitly on each deploy. |
| 37 | + |
| 38 | +## Verification |
| 39 | + |
| 40 | +```bash |
| 41 | +gcloud run services describe policyengine-api-staging \ |
| 42 | + --project policyengine-api --region us-central1 \ |
| 43 | + --format "value(status.url, spec.template.spec.serviceAccountName)" |
| 44 | +gcloud run services get-iam-policy policyengine-api-staging \ |
| 45 | + --project policyengine-api --region us-central1 |
| 46 | +curl -s -o /dev/null -w '%{http_code}\n' <service URL> |
| 47 | +``` |
| 48 | + |
| 49 | +Expected: service URL resolves, runtime service account is |
| 50 | +`policyengine-api-cr-runtime@policyengine-api.iam.gserviceaccount.com`, IAM policy contains |
| 51 | +`allUsers` with `roles/run.invoker`, and the curl returns 200. |
| 52 | + |
| 53 | +## Steady State After Stage 1 |
| 54 | + |
| 55 | +- Staging jobs (`deploy-cloud-run-staging`, `promote-cloud-run-staging`) pin |
| 56 | + `CLOUD_RUN_SERVICE: policyengine-api-staging`; the production job |
| 57 | + (`deploy-cloud-run-candidate`) pins `CLOUD_RUN_SERVICE: policyengine-api`. Unit tests |
| 58 | + enforce that every service-targeting Cloud Run job carries an explicit pin. |
| 59 | +- The Docker image name is decoupled from the service name (`CLOUD_RUN_IMAGE_NAME`): |
| 60 | + production reuses the image built by the staging track, so both tracks push and pull |
| 61 | + `policyengine-api:<sha>` regardless of service. |
| 62 | +- Every API response carries `X-PolicyEngine-Backend` (`app_engine` or `cloud_run`) for |
| 63 | + in-band backend verification during the host cutover traffic ramps. |
| 64 | +- Known follow-up (out of Stage 1 scope): the staging service still uses the prod-named |
| 65 | + Secret Manager secrets and the production Cloud SQL instance with staging DB user/name |
| 66 | + values, as the shared-service track always did. The service split makes a per-service |
| 67 | + secret set possible; migrate in a later stage. |
| 68 | + |
| 69 | +## Exit Gates |
| 70 | + |
| 71 | +Evaluated on the first post-merge push cycle (see the Stage 1 section of |
| 72 | +`api-v1-pr4-cloud-run-host-cutover-execution-plan.md` in the planning folder): |
| 73 | + |
| 74 | +- `gcloud run services describe policyengine-api --region us-central1` shows only |
| 75 | + `stage3-prod-*` tags at 100% at all times, including after the staging promote step. |
| 76 | +- `policyengine-api-staging` is healthy and passes the staging integration suite via its |
| 77 | + own URL. |
| 78 | +- `X-PolicyEngine-Backend` is present and correct on the App Engine production URL, the |
| 79 | + Cloud Run staging service URL, and the Cloud Run production service URL. |
0 commit comments