Operator reference for the Pool Classifier running on Google Cloud Run. For the migration history and design rationale, see the Cloud Run migration history. For local development, see the local setup guide.
| Thing | Value |
|---|---|
| GCP project | relops-pool-classifier (number 410047876591) |
| Org / folder | firefox.gcp.mozilla.com / 723902893592 (the "Hangar"/relops folder) |
| Region | us-west1 |
| Domain | pool-classifier.relops.mozilla.com |
| Load balancer IP | 34.107.179.124 (A record target) |
| Cloud Run service | pool-classifier |
| Cloud Run jobs | pool-classifier-migrate, pool-classifier-db-maintenance, pool-classifier-db-diagnose |
| Cloud Run Console | https://console.cloud.google.com/run/overview?authuser=1&project=relops-pool-classifier |
| Cloud Run URL (LB-only ingress) | https://pool-classifier-gpn5d6lwgq-uw.a.run.app |
| Cloud SQL instance | pool-classifier-db (Postgres 16; db pool_classifier, user pc) |
| Artifact Registry image | us-west1-docker.pkg.dev/relops-pool-classifier/pool-classifier/app |
| Scheduler job | pool-classifier-classify-all (every 15 min) |
| Secrets | pc-db-url (TF-populated), pc-tc-token (manual) |
| Terraform state | gs://moz-relops-tf-state, prefix pool-classifier (project relops-terraform-state) |
Service accounts:
- Runtime:
pool-classifier-run@relops-pool-classifier.iam.gserviceaccount.com - Scheduler:
pool-classifier-scheduler@relops-pool-classifier.iam.gserviceaccount.com - IAP agent:
service-410047876591@gcp-sa-iap.iam.gserviceaccount.com - Cloud Build:
410047876591-compute@developer.gserviceaccount.com(Compute default SA)
pool-classifier.relops.mozilla.com → 34.107.179.124
│
┌─────────────────▼─────────────────┐
Browser (mozilla.com) ──►│ HTTPS LB + Cloud Armor + managed SSL│
│ URL map (pool-classifier-url-map): │
│ /classify* → classify-backend ───┼──► (no IAP, OIDC only)
│ everything → backend (IAP) ──────┼──► (IAP: @mozilla.com)
Cloud Scheduler ────────►│ │
(classify-all, OIDC) └─────────────────┬───────────────────┘
│ (both backends share one serverless NEG)
┌─────────▼──────────┐
│ Cloud Run │──► Cloud SQL (private IP, VPC connector)
│ pool-classifier │──► Secret Manager (pc-db-url, pc-tc-token)
│ ingress=INTERNAL_LB│──► Taskcluster API (public egress)
└────────────────────┘
Two LB backends share one Cloud Run service via one NEG:
pool-classifier-backend— IAP-protected (browsers); serves the dashboard.pool-classifier-classify-backend— no IAP; serves/classify*for Cloud Scheduler. Protected instead by app-level OIDC (auth.py) + Cloud Run's own OIDC check (scheduler SA hasrun.invoker;custom_audienceslets Cloud Run accept the LB-domain audience).
Auth, two paths:
- Dashboard: IAP authenticates the Google user (explicit OAuth client +
provisioned IAP service agent) → checks
roles/iap.httpsResourceAccessor(domain:mozilla.com) → IAP invokes Cloud Run as the IAP service agent. - Scheduler: signs an OIDC token (audience =
https://<domain>/, scheduler SA) → Cloud Run validates it (needscustom_audiences+run.invoker) →auth.pyre-validates the same token.
Classify flow: one Scheduler job → POST /classify-all → the app reads
pools.yaml (registry.all_pools()) and classifies every enabled pool
sequentially, ordered from the previous complete scan's workers refreshed
per second (with deterministic fallback ordering when no history exists).
pools.yaml is the single source of
truth — there is no terraform pool list.
| Method + path | Auth | Purpose |
|---|---|---|
GET /healthz |
none | Liveness (behind IAP at the LB) |
GET / |
IAP | Multi-pool dashboard |
GET /pools/<prov>/<wt> |
IAP | Per-pool page |
GET /pools/<prov>/<wt>/overview.md |
IAP | Markdown report |
GET /pools/<prov>/<wt>/unclassified/<task>.log |
IAP | Streamed unclassified log |
POST /classify/<prov>/<wt> |
OIDC | Classify one pool manually |
POST /classify-all |
OIDC | Sequential classify of all enabled pools (Scheduler / pc_fetch_data.sh) |
Code change → rebuild + redeploy the image (Cloud Build builds the
Dockerfile, pushes to Artifact Registry, gcloud run deploy):
gcloud builds submit --config cloudbuild.yaml \
--substitutions=_TAG=$(git rev-parse --short HEAD),COMMIT_SHA=$(git rev-parse HEAD) \
--project=relops-pool-classifier .Uses
_TAGfor the image tag andCOMMIT_SHAfor the immutable source revision. Cloud Build supplies the latter for triggered builds; manual submits pass it explicitly. Terraform'slifecycle.ignore_changeskeepsapplyfrom reverting the deployed image. The Cloud Run image installs the locked production dependency set frompyproject.tomlanduv.lock.
After deploy, verify the active revision/image and recent app health:
gcloud run services describe pool-classifier --region=us-west1 \
--project=relops-pool-classifier \
--format="value(status.latestReadyRevisionName,status.url,spec.template.spec.containers[0].image)"
gcloud logging read \
'resource.type=cloud_run_revision AND resource.labels.service_name=pool-classifier AND severity>=WARNING' \
--project=relops-pool-classifier --limit=20 --freshness=10m --order=desc \
--format="value(timestamp, severity, textPayload)"Infra change → terraform:
cd worker_health/pool_classifier_web/terraform
terraform plan # review
terraform applySecrets live in the gitignored
terraform.tfvars(db_password, IAP client id/secret). If apply/init errors with a reauth/invalid_raptmessage, refresh Application Default Credentials:gcloud auth application-default login.
Two separate credentials — they expire independently (Mozilla enforces periodic reauth, so expect to re-run these):
# gcloud CLI commands (gcloud ..., scheduler/run/logging/etc.)
gcloud auth login
# Terraform / anything using Application Default Credentials.
# Fixes: "Reauthentication failed" / "invalid_grant" / "invalid_rapt" on
# terraform init/plan/apply.
gcloud auth application-default login# Tail recent app + request logs (most recent first)
gcloud logging read \
'resource.type=cloud_run_revision AND resource.labels.service_name=pool-classifier' \
--project=relops-pool-classifier --limit=50 --freshness=30m \
--order=desc --format="value(timestamp, textPayload)"
# Classify request statuses (200 good; 401 auth; 500 app error)
gcloud logging read \
'resource.type=cloud_run_revision AND resource.labels.service_name=pool-classifier AND httpRequest.requestUrl=~"/classify"' \
--project=relops-pool-classifier --limit=20 --freshness=1h \
--format="table(timestamp, httpRequest.status, httpRequest.latency, httpRequest.requestUrl)"
# Errors/warnings only (tracebacks)
gcloud logging read \
'resource.type=cloud_run_revision AND resource.labels.service_name=pool-classifier AND severity>=WARNING' \
--project=relops-pool-classifier --limit=30 --freshness=1h \
--format="value(timestamp, severity, textPayload)"gcloud scheduler jobs list --location=us-west1 --project=relops-pool-classifier
# Last attempt result (status: {} == success)
gcloud scheduler jobs describe pool-classifier-classify-all \
--location=us-west1 --project=relops-pool-classifier \
--format="yaml(state, lastAttemptTime, status, scheduleTime)"
# Trigger a run now (the sweep runs async; watch logs)
gcloud scheduler jobs run pool-classifier-classify-all \
--location=us-west1 --project=relops-pool-classifier
jobs describecan lag or continue showing the previous failed attempt while a manually triggered/classify-allrun is still active. Watch Cloud Run logs for current-revisionScan donelines and/classify-allrequest status.
gcloud run services describe pool-classifier --region=us-west1 \
--project=relops-pool-classifier \
--format="value(status.latestReadyRevisionName, status.url)"
# Current deployed image
gcloud run services describe pool-classifier --region=us-west1 \
--project=relops-pool-classifier \
--format="value(spec.template.spec.containers[0].image)"
# Change scaling (it's in lifecycle ignore_changes, so edit here, not terraform)
gcloud run services update pool-classifier --region=us-west1 \
--project=relops-pool-classifier --min-instances=0 --max-instances=2gcloud builds list --project=relops-pool-classifier --limit=5 \
--format="table(id, status, createTime, duration)"
gcloud builds log <BUILD_ID> --project=relops-pool-classifier# Connect (opens a temporary public-IP path; or use Cloud SQL Auth Proxy)
gcloud sql connect pool-classifier-db --user=pc --project=relops-pool-classifier
# Useful checks once connected (psql):
# SELECT pool_id, count(*), max(classified_at) FROM task_results GROUP BY 1 ORDER BY 3 DESC;
# SHOW max_connections;
# SELECT count(*) FROM pg_stat_activity; -- connection pressure
# Identify pool-classifier work by Cloud Run revision, instance, and role:
# SELECT pid, application_name, state, wait_event_type, wait_event,
# now() - query_start AS query_age, query
# FROM pg_stat_activity
# WHERE application_name LIKE 'pool-classifier:%'
# ORDER BY query_start;
# Roles are web, classifier, migration, advisory-lock, and maintenance. For a
# blocked classifier, find its advisory-lock backend above, then inspect the
# same PID in pg_locks to identify the held or waited-on lock.# IAP members on the dashboard backend
gcloud iap web get-iam-policy --resource-type=backend-services \
--service=pool-classifier-backend --project=relops-pool-classifier
# Managed cert status (want managed.status=ACTIVE)
gcloud compute ssl-certificates list --project=relops-pool-classifier \
--format="table(name, managed.status, managed.domainStatus)"
# DNS resolves to the LB IP?
dig +short pool-classifier.relops.mozilla.com Agcloud secrets versions list pc-tc-token --project=relops-pool-classifier
# Rotate the Taskcluster token (JSON: {"clientId":"...","accessToken":"..."})
gcloud secrets versions add pc-tc-token --data-file=$HOME/.tc_token \
--project=relops-pool-classifierThese bit us during the migration; documented so they don't again.
- IAP + Cloud Run needs the IAP service agent as invoker. Provision the IAP
service identity and grant it
run.invoker(terraform:run.tf). Symptom: "The IAP service account is not provisioned" / Cloud Run 403. - Scheduler OIDC needs
custom_audienceson Cloud Run. Scheduler signs the token with the LB-domain audience; Cloud Run rejects it (401 access token could not be verified) unless the domain is incustom_audiences(run.tf). - OAuth consent screen + OAuth client are manual. The IAP OAuth Admin API was
shut down (Mar 2026); there is no terraform for these. Create them in Console
(Google Auth Platform): consent screen External / In production, plus a Web
OAuth client whose redirect URI is
https://iap.googleapis.com/v1/oauth/clientIds/<id>:handleRedirect. Put the client id/secret interraform.tfvars. @mozilla.comis a separate Cloud Identity from thefirefox.gcp.mozilla.comorg.domain:mozilla.comworks as the IAP principal; "Internal" consent audience does NOT (it scopes to the org).- Cloud Build runs as the Compute Engine default SA on this (post-2024)
project, not the legacy
@cloudbuildSA. It has builder/run.admin/AR-writer/ serviceAccountUser (iam.tf). db-g1-smallhas a low connection limit. The app uses one process-localpsycopg_poolper DSN, shared by cached pool classifiers. Pool size is controlled byPC_DB_POOL_MIN/PC_DB_POOL_MAX(defaults:1/5). KeepGUNICORN_WORKERS=1,cloud_run_max_instances=2, and the single sequential/classify-alljob unless the Cloud SQL connection budget is raised. Symptom if exceeded: "remaining connection slots are reserved for roles with privileges of pg_use_reserved_connections".- IAP protects browser paths. CLI
curlto dashboard URLs such as/or/favicon.iconormally returns an IAP OAuth redirect unless you provide an authenticated IAP request. For deploy verification, prefer Cloud Run logs, current revision/image checks, or an authenticated browser. - Cloud Run scaling drift.
gcloud run deployre-stamps the scaling block, so it's inlifecycle.ignore_changes; change scaling viagcloud run services update, not terraform. - ADC reauth. Terraform uses Application Default Credentials; Mozilla enforces
periodic reauth.
gcloud auth application-default loginwhen it expires. .gcloudignorekeeps the Cloud Build source upload small (excludespgdata/,.terraform/, run dirs) — separate from.dockerignore.