Free lesson · GenAI Platform Engineering

Automate image builds with GitHub Actions

You will build a CI pipeline that automatically builds and pushes Docker images. Create a workflow triggered on pushes to main and on tags matching v*. Authenticate to Google Artifact Registry using Workload Identity Federation (no JSON key files). Build the image with docker/build-push-action, pushing to {region}-docker.pkg.dev/{project}/ai-services/{app}. Implement tagging strategy: main pushes get sha-{short_sha} and latest tags, version tags get the semver tag (v1.2.3). Cache Docker layers using GitHub Actions cache backend (type=gha) for faster builds. Add a build summary as a GitHub Actions step output showing image size and build duration.

Course: DevOps Foundations for GenAI Engineers · Chapter 3 · Container Image CI/CD

Free to read — no subscription required.

Introduction

When an engineer builds an image locally with docker build, pushes with docker push, and announces in Slack that a new tag is ready, the process is unrepeatable, unaudited, and fragile. Two laptops with different Docker versions, different cached layers, or different environment variables produce subtly different images from the same Dockerfile — and the resulting drift surfaces as "works on my machine" incidents days later in staging. Automating the build in CI eliminates that variance: every image comes from an identical, ephemeral environment with full provenance from Git commit to registry artifact. By the end of this lesson you will be able to configure a GitHub Actions workflow that builds, tags, and pushes a container image to Google Artifact Registry on every push to main, authenticates without long-lived keys, and rebuilds code-only changes in under three minutes.

Key Terminology

  • GitHub Actions workflow — a YAML file in .github/workflows/ that defines the triggers, jobs, and steps CI runs; this is where the entire build pipeline lives.
  • Workload Identity Federation (WIF) — a trust relationship between GitHub's OIDC provider and Google Cloud IAM that lets a workflow exchange a short-lived OIDC token for a Google access token, removing the need to store a service account JSON key.
  • Artifact Registry — Google Cloud's managed container image registry; the destination this lesson's workflow pushes to and the source production deployments pull from.
  • Layer cache (type=gha) — the GitHub Actions cache backend for docker/build-push-action that stores Docker layer data between runs so dependency-heavy layers like pip install are reused when requirements.txt is unchanged.
  • Image tag — a human- or commit-derived label attached to an image digest; this lesson uses sha-{short_sha} for traceability and latest for rolling consumers.

Concepts

Determinism through ephemeral CI runners

Every workflow run starts on a fresh ubuntu-latest runner with no carried-over state, so the only inputs to the build are the repository contents at the triggering commit and the workflow definition itself. That eliminates the laptop-to-laptop drift manual builds suffer. The trigger surface — push to main plus v* tags — covers continuous integration of trunk and explicit release builds with a single workflow file.

Keyless authentication with Workload Identity Federation

WIF replaces a stored service account JSON key with a short-lived token exchange. The workflow asks GitHub's OIDC provider for a JWT carrying repository and branch claims, hands that JWT to Google Cloud IAM, and receives a one-hour access token scoped to a specific service account. The trust policy on the Google side can pin authentication to a specific repository, branch, or even workflow file, so a compromised fork cannot push to your registry.

Loading diagram...

Three properties matter for production. The token expires in an hour, bounding leak blast radius. The trust policy whitelists specific repos and refs. No key ever needs rotation because every run mints a fresh token.

Deterministic, traceable tagging

docker/metadata-action derives tags from the Git context: sha-{short_sha} on every build (a unique pointer back to the commit), the Git ref on tag pushes (release versioning), and a rolling latest (consumer convenience). Every artifact in the registry maps back to one commit, which is what lets downstream incident response trace a misbehaving pod to a specific change. See Code Walkthrough for the concrete tag block.

Layer caching with type=gha

The GitHub Actions cache backend (up to 10 GB per repo) stores Docker layer blobs between runs. For AI service images whose pip install layer takes five-to-eight minutes, restoring that layer from cache when requirements.txt is unchanged cuts a twelve-minute cold build to roughly two minutes — comfortably under the three-minute target for code-only changes. mode=max exports all stages of a multi-stage build, not just the final one, which maximizes hit rate when builder layers change rarely.

Code Walkthrough

The workflow below ties all four concepts together — ephemeral runner, WIF authentication, deterministic tagging, and type=gha caching — into a single .github/workflows/build-push.yml file. Read it top-down: triggers, permissions for OIDC, then the steps that authenticate, derive tags, and push.

Code snippetyaml
1name: Build and Push Container Image 2 3on: 4 push: 5 branches: [main] 6 tags: ["v*"] 7 8permissions: 9 contents: read 10 id-token: write 11 12jobs: 13 build: 14 runs-on: ubuntu-latest 15 steps: 16 - name: Checkout code 17 uses: actions/checkout@v4 18 19 - name: Authenticate to Google Cloud 20 id: auth 21 uses: google-github-actions/auth@v2 22 with: 23 workload_identity_provider: ${{ vars.WIF_PROVIDER }} 24 service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} 25 26 - name: Configure Docker for Artifact Registry 27 run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet 28 29 - name: Extract metadata 30 id: meta 31 uses: docker/metadata-action@v5 32 with: 33 images: us-central1-docker.pkg.dev/${{ vars.GCP_PROJECT }}/ai-services/inference 34 tags: | 35 type=sha,prefix=sha-,format=short 36 type=ref,event=tag 37 type=raw,value=latest 38 39 - name: Set up Docker Buildx 40 uses: docker/setup-buildx-action@v3 41 42 - name: Build and push image 43 uses: docker/build-push-action@v5 44 with: 45 context: . 46 push: true 47 tags: ${{ steps.meta.outputs.tags }} 48 labels: ${{ steps.meta.outputs.labels }} 49 cache-from: type=gha 50 cache-to: type=gha,mode=max
  • on: triggers a run for every push to main and every v* tag, covering both CI of trunk and explicit releases through one workflow.
  • permissions: grants the job the id-token: write capability required to mint an OIDC JWT for WIF; contents: read is the minimum for checkout. Nothing else is granted.
  • google-github-actions/auth@v2 performs the JWT-for-access-token exchange shown in the Concepts sequence diagram. WIF_PROVIDER and GCP_SERVICE_ACCOUNT are stored as repository variables — not secrets — because neither value is sensitive in the WIF model.
  • docker/metadata-action@v5 emits sha-{short_sha} on every run, the ref name on tag pushes, and a rolling latest — the three-tag strategy from the tagging concept.
  • docker/build-push-action@v5 with cache-from: type=gha and cache-to: type=gha,mode=max is the layer-cache wiring; mode=max is the lever that keeps multi-stage builder layers warm between runs.

You'll know it works when a push to main produces a green run in the Actions tab whose logs show CACHED against the dependency layer on the second run, and Artifact Registry contains a fresh image tagged sha-<short> plus an updated latest — with no JSON key file anywhere in the repository, GitHub Secrets, or developer laptops, and code-only rebuilds finishing under three minutes.

Do's and Don'ts

Do's

  1. Do pin action versions to a major tag@v4, @v5 — so dependabot can surface breaking upgrades without your workflow silently changing behavior on every run.
  2. Do store WIF_PROVIDER and the service account email as vars, not secrets — they aren't sensitive, and using vars lets you read them in pull-request UIs and audit logs without redaction.
  3. Do tag every image with sha-{short_sha} — it's the only tag that uniquely maps a running container back to one Git commit during incident response.

Don'ts

  1. Don't commit a service account JSON key — even briefly, even in a private repo; GitHub's secret scanning will flag it and rotation is operationally painful. WIF removes the need entirely.
  2. Don't rely on latest for deployments — it's a convenience pointer, not an immutable reference; production rollouts must pin to sha-{short_sha} or a release tag so rollback is well-defined.
  3. Don't skip cache-to: type=gha,mode=max — without it only the final stage caches, and multi-stage AI images lose the dependency-layer reuse that keeps code-only builds under three minutes.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Platform Engineering subscription.

From · cancel anytime

More free lessons in DevOps Foundations for GenAI Engineers

All free lessons in GenAI Platform Engineering