Table of Contents#
- What Cloud Build Actually Does
- Anatomy of a Build: Steps, Substitutions, and the Shared Workspace
- Triggers: What Starts a Build
- Artifact Registry: Where the Build's Output Lives
- Private Pools: Building Inside Your Own VPC
- Designing a Pipeline: What CI Actually Needs to Verify
- Monorepos: Path-Filtered Triggers Instead of One Trigger Per Service
- Third-Party Tooling: Cloud Build Is Not GCP-Exclusive
- A Full Worked Pipeline: Meridian's
shipment-apiBuild - Common Mistakes and Interview Traps
- Worked Practice Problems
- Summary and What's Next
What Cloud Build Actually Does#
Cloud Build takes a source change and turns it into a tested, versioned, deployable artifact — nothing more, nothing less. It does not deploy anything itself; that's Cloud Deploy's job, covered in Part 4. Cloud Build's entire job is the "CI" half of "CI/CD": compile, test, scan, and package, ending with an artifact sitting in Artifact Registry, ready for something else to promote it.
Every Cloud Build run executes as a sequence of steps, each one a container image running a command against a shared workspace — the same filesystem checked out from source, visible to every step in order. This container-per-step model is what makes a build config portable and predictable: a step that runs go build is just "run this container image with this command," not a magic built-in Cloud Build feature, which means a team can swap in any container image (a custom linter, a proprietary scanner) as easily as reaching for one of Google's own prebuilt builder images.
What to notice: every step shares the same workspace, so Step 3's built binary is exactly the file Step 2's test run just verified — there's no separate copy or handoff step, which is what makes "what was tested is what ships" (Part 1's Artifact Registry principle) an actual guarantee rather than an assumption.
Anatomy of a Build: Steps, Substitutions, and the Shared Workspace#
A cloudbuild.yaml file is the build's own declaration — checked into the source repository itself, so the build definition is versioned right alongside the code it builds.
# cloudbuild.yaml for Meridian's shipment-api — real steps, real
# substitutions, nothing abbreviated for the tutorial
steps:
- id: 'run-unit-tests'
name: 'golang:1.24'
entrypoint: 'go'
args: ['test', './...']
- id: 'build-image'
name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/meridian-images/shipment-api:${SHORT_SHA}'
- '.'
- id: 'push-image'
name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/meridian-images/shipment-api:${SHORT_SHA}'
substitutions:
_REGION: 'us-central1'
images:
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/meridian-images/shipment-api:${SHORT_SHA}'
options:
logging: CLOUD_LOGGING_ONLYTwo kinds of substitution variables show up here, and knowing which is which matters: ${PROJECT_ID} and ${SHORT_SHA} are Cloud Build's own built-in substitutions — Cloud Build fills these in automatically from the triggering event, with no declaration needed. ${_REGION} is a user-defined substitution — the leading underscore is mandatory, it's how Cloud Build tells the two kinds apart, and it's declared explicitly in the substitutions: block (or overridden per-trigger without touching the YAML file at all).
| Substitution | Kind | Resolves to |
|---|---|---|
$PROJECT_ID | Built-in | The project running the build |
$SHORT_SHA / $COMMIT_SHA | Built-in | The triggering commit, short or full form |
$BRANCH_NAME / $TAG_NAME | Built-in | The Git ref that triggered the build |
$TRIGGER_NAME | Built-in | Which trigger configuration fired |
$_REGION, $_ENVIRONMENT, any _-prefixed name | User-defined | Whatever the trigger config or YAML declares |
Tip
Best Practice: always tag the built image with $SHORT_SHA (or $COMMIT_SHA), never latest, as shown above. An image tagged latest gives you no way to answer "which commit is actually running in production right now" — a question you will need to answer under real incident pressure, not just in a design review.
Triggers: What Starts a Build#
A trigger is the binding between a source event and a build config — without one, cloudbuild.yaml is just an inert file sitting in a repository. Cloud Build supports several trigger event types, and picking the right one per pipeline stage is a real design decision, not a default to leave alone.
| Trigger type | Fires on | Typical use |
|---|---|---|
| Push to branch | A commit lands on a matching branch (regex-filterable) | CI on every main commit |
| Pull request | A PR is opened/updated against a target branch | Run tests before merge, without pushing a deployable artifact |
| Tag push | A Git tag matching a pattern is pushed | Cutting a release build separately from every-commit CI |
| Manual | A human or API call explicitly starts it | An on-demand rebuild, a manual promotion trigger |
| Webhook | An arbitrary external HTTP call | Integrating a non-Git event source into the pipeline |
| Pub/Sub | A message on a specified topic | Chaining builds together, or triggering from a non-source-control event |
🔍 From the Trenches: A team configured a single Cloud Build trigger to fire on push to any branch, intending it as a convenience so feature-branch commits got CI coverage too. Six weeks later, a contractor pushed forty small commits to a long-lived experimental branch over one afternoon while debugging locally, and the resulting forty separate builds — each one pulling dependencies, running the full test suite, and pushing an image to Artifact Registry — burned through the team's monthly Cloud Build budget in a single day and pushed forty untagged, never-to-be-used images into the shared registry. The two-levels-deep lesson: the surface symptom was "budget alert fired unexpectedly," the immediate cause was an unfiltered branch trigger, and the underlying condition was that nobody had actually decided which branches warranted a full build-and-push versus a lighter test-only check — the trigger config reflected "make CI happen somehow," not a deliberate pipeline design.
Tip
Best Practice: use a branch-name regex filter (^main$, or ^release/.*$) on push triggers that build and push artifacts, and reserve pull-request triggers — which can run tests without pushing anything — for feature-branch coverage. This is exactly the mistake in the callout above, and the fix costs one line of trigger configuration.
Artifact Registry: Where the Build's Output Lives#
Artifact Registry is GCP's multi-format package registry — one product handling Docker images, Maven artifacts, npm packages, Python wheels, Go modules, and OS packages (Apt/Yum), rather than a separate product per artifact type. Three repository modes matter for a CI/CD pipeline specifically:
- Standard repository — the one used throughout this chapter so far: you push artifacts your own builds produce, and it stores them.
- Remote repository — a proxy-and-cache in front of an upstream public registry (Docker Hub, Maven Central, PyPI, npm). A build pulling a public dependency through a remote repository gets caching (faster, more resilient builds) and a single point where Artifact Analysis can scan cached upstream packages for known vulnerabilities before your build ever consumes them.
- Virtual repository — a single endpoint unifying several standard and/or remote repositories behind one URL, with priority ordering between upstreams to guard against dependency confusion (a malicious public package with the same name as an internal one). Vulnerability scanning is not supported inside a virtual repository itself — scanning happens at the standard/remote repositories it unifies, a real, easy-to-miss limitation worth knowing for the exam.
What to notice: a build never needs to know whether a given dependency actually lives inside Meridian's own registry or is being proxied from Docker Hub — the virtual repository's single URL hides that distinction, which is exactly what protects against a compromised or typo-squatted public package silently shadowing an intended internal one.
Warning
Automatic vulnerability scanning (via Artifact Analysis, covered in depth in Part 6) does not run inside a virtual repository. If your pipeline pulls dependencies exclusively through a virtual repo's single URL, confirm the underlying standard/remote repositories it unifies actually have scanning enabled — a virtual repo can silently give a false sense of coverage otherwise.
Private Pools: Building Inside Your Own VPC#
Cloud Build's default execution environment runs builds in a Google-managed pool with no network path into your VPC — fine for a build that only needs public internet access, but a real gap the moment a build step needs to reach a private resource: a Cloud SQL instance on a private IP, an internal artifact mirror, or anything behind a VPC Service Controls perimeter.
A private pool is a dedicated Cloud Build worker pool provisioned inside your own VPC, giving builds a real network identity and route into private infrastructure.
# Reference a private pool from cloudbuild.yaml — the pool itself is
# provisioned separately (via gcloud builds worker-pools create), this
# just tells THIS build to run inside it instead of the shared default
options:
pool:
name: 'projects/meridian-cicd/locations/us-central1/workerPools/meridian-vpc-pool'| Aspect | Default (shared) pool | Private pool |
|---|---|---|
| Network location | Google-managed, no VPC access | Runs inside your VPC, real internal IP |
| Reaches private Cloud SQL/on-prem | No | Yes |
| VPC Service Controls compatible | Only via public API access | Yes — can sit fully inside a perimeter |
| Cost model | Per build-minute, no idle cost | Per build-minute plus the pool's own provisioning |
| Setup | None | Provision the pool once, reference it per trigger/build |
Designing a Pipeline: What CI Actually Needs to Verify#
The exam guide's "designing pipelines" section (2.1) is explicitly about more than "run the tests" — a real CI stage earns its place in a pipeline by catching specific classes of problems before they reach a human reviewer or a later, more expensive stage.
At minimum, a production CI pipeline should verify, in roughly this order (fail fast on the cheapest checks first):
- Static checks — linting, formatting, a type-checker — seconds, not minutes, and catches the highest volume of trivial issues before spending compute on anything heavier.
- Unit tests — fast, no external dependencies, the bulk of test coverage.
- Build the artifact — compile/package; a build failure here is a real signal, not a flaky test.
- Integration tests — against real (or realistic, containerized) dependencies; slower, so run after the artifact is known to build cleanly.
- Vulnerability scan — Artifact Analysis against the just-built image, covered in depth in Part 6.
- Push to Artifact Registry — only after every prior gate passes.
💡 The transferable insight: this ordering is the same "fail cheap and fast before failing expensive and slow" discipline manufacturing lines apply with early inline quality checks before a part ever reaches final assembly — catching a defect at the first station is far cheaper than discovering it after the whole product is built.
Monorepos: Path-Filtered Triggers Instead of One Trigger Per Service#
Meridian's orders database access layer, shipment-api, and the GPS ingestion pipeline all live in one Git repository — a monorepo, chosen deliberately so a shared library change and its consuming services get reviewed together. A naive push trigger covering the whole repository would rebuild and retest every service on every commit, even a one-line change to a service nobody touched — directly repeating the wasted-build-minutes problem from this chapter's earlier From-the-Trenches callout, just with a different root cause.
Cloud Build's triggers support includedFiles/ignoredFiles path filters specifically for this: a trigger only fires when the pushed commit actually touches a matching path.
# shipment-api's trigger only fires when a commit touches its own
# directory or the shared library it depends on -- a commit that only
# touches gps-ingestion/ never wakes this trigger up at all
gcloud builds triggers create github \
--project=meridian-cicd \
--name=shipment-api-monorepo \
--repo-name=meridian-platform \
--repo-owner=meridian-logistics \
--branch-pattern="^main$" \
--build-config=services/shipment-api/cloudbuild.yaml \
--included-files="services/shipment-api/**,libs/shared-auth/**"What to notice: each service in the monorepo has its own trigger with its own path filter, rather than one trigger with branching logic inside a single cloudbuild.yaml — this keeps each service's build config independently reviewable and matches the "one trigger, one clear responsibility" shape this chapter's trigger-type table already established.
Tip
Best Practice: always include a repo's genuinely shared libraries in every dependent service's includedFiles pattern, as shown above with libs/shared-auth/**. A monorepo's most common CI gap isn't over-triggering — it's under-triggering: a shared library change that silently doesn't retrigger a consuming service's tests, because its path filter only covers that service's own directory.
Third-Party Tooling: Cloud Build Is Not GCP-Exclusive#
The PCDE exam guide explicitly names "widely used third-party tooling (e.g., Git, Jenkins, Argo CD, Packer, kpt)" as fair game — a real signal that the certification tests CI/CD concepts, not just Google's own product surface. A few concrete ways this shows up in practice:
- Jenkins can trigger Cloud Build jobs via its API, or run entirely independently with GCP as just a deploy target — common in organizations mid-migration from an existing Jenkins investment.
- Packer builds machine images (VM images, not containers) and commonly runs as a Cloud Build step, producing a golden image that a later Terraform
applyreferences. - kpt manages Kubernetes configuration packages declaratively, often alongside or instead of raw Kustomize, and integrates with Config Sync from Part 2.
- Argo CD is a popular self-hosted alternative to Config Sync for Kubernetes GitOps — the exam's tool-agnostic framing means you're expected to recognize when Argo CD's model fits (multi-cloud Kubernetes fleets not exclusively on GKE, an existing Argo CD investment) versus when Config Sync's tighter GKE-native integration wins.
Note
Don't over-rotate toward memorizing every third-party tool's flag syntax — the exam tests whether you recognize a tool's role in a pipeline (a build trigger, a manifest renderer, a GitOps reconciler) and can reason about tradeoffs, not vendor-specific trivia for tools GCP doesn't itself ship.
A Full Worked Pipeline: Meridian's shipment-api Build#
Bringing this chapter's pieces together — the actual trigger and build configuration Priya's team runs today, incorporating the branch filtering lesson from the earlier From-the-Trenches callout and the fail-fast ordering from the design section above.
# Create the push trigger — filtered to main only, per the lesson
# earlier in this chapter, running in the tooling project's private
# pool so integration tests can reach the staging Cloud SQL instance
# over a private IP
gcloud builds triggers create github \
--project=meridian-cicd \
--name=shipment-api-main \
--repo-name=shipment-api \
--repo-owner=meridian-logistics \
--branch-pattern="^main$" \
--build-config=cloudbuild.yaml \
--substitutions=_REGION=us-central1# cloudbuild.yaml — the fail-fast ordering from this chapter's design
# section, expressed as real steps
steps:
- id: 'lint'
name: 'golangci/golangci-lint:latest'
args: ['run', './...']
- id: 'unit-tests'
name: 'golang:1.24'
entrypoint: 'go'
args: ['test', '-short', './...']
waitFor: ['lint']
- id: 'build-image'
name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', '${_REGION}-docker.pkg.dev/${PROJECT_ID}/meridian-images/shipment-api:${SHORT_SHA}', '.']
waitFor: ['unit-tests']
- id: 'integration-tests'
name: 'golang:1.24'
entrypoint: 'go'
args: ['test', '-run', 'Integration', './...']
env:
- 'DB_HOST=10.10.0.5' # private IP, reachable only via the private pool
waitFor: ['build-image']
- id: 'push-image'
name: 'gcr.io/cloud-builders/docker'
args: ['push', '${_REGION}-docker.pkg.dev/${PROJECT_ID}/meridian-images/shipment-api:${SHORT_SHA}']
waitFor: ['integration-tests']
options:
pool:
name: 'projects/meridian-cicd/locations/us-central1/workerPools/meridian-vpc-pool'
logging: CLOUD_LOGGING_ONLY
images:
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/meridian-images/shipment-api:${SHORT_SHA}'🧪 Hands-on checkpoint: note the explicit waitFor on every step — without it, Cloud Build runs steps with no declared dependency in parallel by default, which would let integration-tests start against an image build-image hasn't finished producing yet. Confirm your own build config's dependency chain is explicit rather than relying on steps happening to be listed in the right order.
Common Mistakes and Interview Traps#
| Mistake | Why it's wrong | What to say/do instead |
|---|---|---|
Tagging every built image latest | No way to trace which commit is actually deployed during an incident | Tag with $SHORT_SHA/$COMMIT_SHA on every build |
| An unfiltered push trigger on every branch | Burns budget and registry space on throwaway feature-branch churn | Filter push triggers to main/release/*; use PR triggers for feature-branch coverage |
| Assuming a virtual repository is scanned | Artifact Analysis does not scan inside virtual repositories | Confirm scanning on the underlying standard/remote repos it unifies |
| Relying on step order alone for dependencies | Steps with no explicit waitFor can run in parallel | Declare waitFor explicitly for every real dependency between steps |
| Running every build in the default shared pool | Can't reach private Cloud SQL, on-prem systems, or a VPC-SC-perimetered resource | Provision a private pool for any build needing private network access |
| Treating vulnerability scanning as a nice-to-have | A shipped critical CVE is a real production risk, not a compliance checkbox | Fail the build on critical/high findings before the image is pushed (Part 6 goes deeper) |
Worked Practice Problems#
Problem 1: A cloudbuild.yaml has three steps with no waitFor fields, and a teammate insists this is fine because "they're listed in the right order." What's actually true about execution order here, and why might this be a real bug rather than a style nitpick?
Answer: Without an explicit waitFor, Cloud Build does not guarantee sequential execution by list order — steps with no declared dependency chain can run in parallel. If step 2 depends on an artifact step 1 produces (a compiled binary a later step copies into a Docker image, for instance) and both happen to run concurrently, the build can intermittently fail — or worse, intermittently succeed with a stale artifact — depending on timing. This is a real correctness bug, not a style preference, and the fix is declaring waitFor on every step with a real dependency.
Problem 2: A team wants their CI pipeline to reach a Cloud SQL instance over its private IP for integration tests, but every build currently runs in Cloud Build's default pool and integration tests fail with a connection timeout. What's the fix, and what's one cost tradeoff it introduces?
Answer: Provision and reference a private pool inside the same VPC as the Cloud SQL instance — the default shared pool has no route into any customer VPC at all, so no firewall rule or IP allowlisting can fix a default-pool build's private connectivity; the pool itself has to change. The tradeoff is cost and operational overhead: a private pool has its own provisioning cost on top of per-build-minute charges, and the team now owns keeping that pool's own networking (subnet sizing, firewall rules) correctly configured going forward.
Problem 3: A pipeline pulls its base Docker image through a virtual Artifact Registry repository that unifies a standard repo and a Docker Hub remote repo. A security review asks to confirm vulnerability scanning covers every image the pipeline consumes. What should you check, and why isn't "the virtual repo has scanning enabled" a valid answer?
Answer: Scanning is not supported inside a virtual repository itself — the correct check is whether the underlying standard and remote repositories the virtual repo unifies each have Artifact Analysis scanning enabled independently. "The virtual repo has scanning enabled" isn't a coherent answer to give in this review, because that setting doesn't exist at the virtual-repo level at all; the security review needs to trace through to the constituent repos.
Summary and What's Next#
This chapter covered the "CI" half of CI/CD in full depth: Cloud Build's container-per-step execution model, the substitution variables that keep a build config reusable, the trigger types and the branch-filtering discipline that keeps them from becoming a cost and clutter problem, Artifact Registry's three repository modes and where vulnerability scanning does and doesn't apply, private pools for reaching VPC-internal resources, and a fail-fast pipeline design ordering that catches cheap problems before expensive ones. Meridian's shipment-api build ties every piece together into one real, working configuration.
Part 4 picks up exactly where this chapter's pipeline ends — the image sitting in Artifact Registry — and covers Cloud Deploy: how that artifact actually gets promoted through dev, staging, and production with canary rollouts, approval gates, and automated rollback.