Cloud Build¶
Four triggers, one service account, two output repos. The triggers fire on activity in the tomoda-labs/tomoda GitHub repo and push Docker images to Artifact Registry.
Code: infrastructure/gcp/cloudbuild.tf, infrastructure/gcp/cloudbuild_iam.tf.
Pipeline definitions (cloudbuild-backend.yaml, cloudbuild-frontend.yaml) live in the tomoda repo, not here — see docs/infrastructure/ in that repo.
Trigger matrix¶
| Trigger | Fires on | Target repo | Approval |
|---|---|---|---|
backend-push-trigger |
Push to main, paths backend/** or cloudbuild-backend.yaml |
tomoda-dev-repo |
Auto |
frontend-push-trigger |
Push to main, paths frontend/** or cloudbuild-frontend.yaml |
tomoda-dev-repo |
Auto |
backend-release-trigger |
Tag matching ^v[0-9]+\.[0-9]+\.[0-9]+$ |
tomoda-prod-repo |
Auto (tag) |
frontend-release-trigger |
Tag matching ^v[0-9]+\.[0-9]+\.[0-9]+$ |
tomoda-prod-repo |
Auto (tag) |
Backend + web release together on one vX.Y.Z tag — both triggers fire on it,
so a single version ships the whole server-delivered app. The native mobile
app versions on its own cadence (its marketing version in app.config.js, plus
OTA), not this tag; see the app repo's docs/frontend/native/release.md. A merge
to main auto-deploys backend + web to dev.
mainv*v* tag ships backend + web together; native versions separately.Tag-based prod = single source of truth
The only way to get an image into tomoda-prod-repo is to push a vX.Y.Z tag to main (backend + web build together). There is no manual docker push, no per-environment branch, and no release/* branch — the tag itself is the release artifact.
Substitutions¶
Triggers pass environment-specific values into the YAML via substitutions. Both YAML files live in the tomoda repo; this Terraform code only sets the variables.
Common to all four triggers¶
| Substitution | Source |
|---|---|
_REGION |
var.region (always us-central1) |
_REPO_NAME |
dev triggers → tomoda-dev-repo; release triggers → tomoda-prod-repo |
Dev push triggers add¶
| Substitution | Value |
|---|---|
TAG_NAME |
latest (push triggers re-tag latest on every successful build) |
Frontend triggers add¶
| Substitution | Dev value | Prod value |
|---|---|---|
_WS_URL |
wss://api-dev.tomoda.life/ws |
wss://api.tomoda.life/ws |
_FRONTEND_API_URL |
https://api-dev.tomoda.life/api/v1 |
https://api.tomoda.life/api/v1 |
_WEB_URL |
https://app-dev.tomoda.life |
https://tomoda.life |
_GOOGLE_WEB_CLIENT_ID |
dev web client | prod web client |
_GOOGLE_IOS_CLIENT_ID |
shared iOS client | shared iOS client |
_GOOGLE_ANDROID_CLIENT_ID |
shared Android client | shared Android client |
One mobile OAuth client per platform
iOS/Android reuse a single OAuth client across dev + prod — an iOS client is keyed on the bundle id (com.tomoda.app) and Android on package + signing SHA-1, so one client per platform serves both. Add the prod release signing SHA-1 to the Android client before shipping prod Android.
Service account & IAM¶
All four triggers use a single dedicated SA: cloudbuild-worker-sa@${project_id}.iam.gserviceaccount.com. The default Cloud Build SA (287267207777@cloudbuild.gserviceaccount.com) is granted roles/iam.serviceAccountUser on it so the platform can impersonate it when running the build.
| Role | What it enables |
|---|---|
roles/logging.logWriter |
Builds can stream logs to Cloud Logging |
roles/artifactregistry.writer |
Push image layers to both dev and prod repos |
roles/container.developer |
Reserved for future post-build kubectl steps (not currently used) |
roles/container.developer is unused: deployment runs via Argo CD Image Updater watching the registry, not a Cloud Build kubectl step.
Gating¶
Dev triggers deploy automatically: a merge to main builds and rolls out to the
dev namespace, so dev always reflects main. The prod gate is the vX.Y.Z tag,
which ships backend + web together. No Cloud Build trigger has a manual approval step.
Build outputs¶
Every successful build pushes a Docker image to the configured Artifact Registry repo. From there:
- Dev: Argo CD Image Updater detects the new
:latestdigest and patches the dev Argo CD application's image tag. - Prod: same flow, but pinned to the semver tag the trigger built.
See Artifact Registry for the repo layout and Argo CD for the GitOps side.
Multi-arch images¶
The cluster runs ARM nodes (GKE), so every built image is a multi-arch manifest (linux/amd64 + linux/arm64) built with docker buildx:
- App images — backend + frontend, via the app repo's
cloudbuild-backend.yaml/cloudbuild-frontend.yaml. - Devops images — postgres, arc-runner, cost-exporter, via this repo's
cloudbuild-postgres.yaml,cloudbuild-arc-runner.yaml,cloudbuild-cost-exporter.yaml.
The arm64 legs of CGO / from-source images (backend, postgres) build under QEMU emulation (tonistiigi/binfmt), slower than native amd64-only. The photon-indexer image is the exception — not multi-arch, so its index build runs out-of-cluster (see Photon).
Vulnerability scanning¶
Artifact Registry's automatic vulnerability scanning is off (containerscanning.googleapis.com not enabled): $0.26 per image scanned, ~$40/mo at dev's build frequency.
Scanning belongs in the build: add a trivy (or grype) step to cloudbuild-backend.yaml / cloudbuild-frontend.yaml in the tomoda monorepo that fails on HIGH/CRITICAL CVEs. Free, blocks a bad image before the registry, lives next to the code. Lands with the app-side CI work. Revisit AR's built-in scan only for continuous re-scanning of already-pushed images against newly-disclosed CVEs (which build-time trivy doesn't cover).
When triggers don't fire¶
Common reasons a push to main doesn't queue a build:
- No matching files changed.
backend-push-triggerhasincluded_files = ["backend/**", "cloudbuild-backend.yaml"]. A PR that only touchesdocs/orfrontend/will not start a backend build. - GitHub App permissions. The Cloud Build GitHub App must remain installed on the
tomoda-labs/tomodarepo. Removing it silently disables every trigger. - Wrong tag shape for prod. A prod build only fires for
vX.Y.Z; any other tag is ignored.