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 fordocker/build-push-actionthat stores Docker layer data between runs so dependency-heavy layers likepip installare reused whenrequirements.txtis unchanged. - Image tag — a human- or commit-derived label attached to an image digest; this lesson uses
sha-{short_sha}for traceability andlatestfor 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.
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 tomainand everyv*tag, covering both CI of trunk and explicit releases through one workflow.permissions:grants the job theid-token: writecapability required to mint an OIDC JWT for WIF;contents: readis the minimum for checkout. Nothing else is granted.google-github-actions/auth@v2performs the JWT-for-access-token exchange shown in the Concepts sequence diagram.WIF_PROVIDERandGCP_SERVICE_ACCOUNTare stored as repository variables — not secrets — because neither value is sensitive in the WIF model.docker/metadata-action@v5emitssha-{short_sha}on every run, the ref name on tag pushes, and a rollinglatest— the three-tag strategy from the tagging concept.docker/build-push-action@v5withcache-from: type=ghaandcache-to: type=gha,mode=maxis the layer-cache wiring;mode=maxis 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
- ✓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. - ✓Do store
WIF_PROVIDERand the service account email asvars, notsecrets— they aren't sensitive, and usingvarslets you read them in pull-request UIs and audit logs without redaction. - ✓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
- ✗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.
- ✗Don't rely on
latestfor deployments — it's a convenience pointer, not an immutable reference; production rollouts must pin tosha-{short_sha}or a release tag so rollback is well-defined. - ✗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
- Ch 2Compare CI platforms: GitHub Actions vs Tekton vs Dagger
- Ch 2Configure CI to run on GKE self-hosted runners
- Ch 3Build optimized Docker images for AI applications
- Ch 3Automate image builds with GitHub ActionsYou are here
- Ch 3Sign images with Cosign and enforce Binary Authorization on GKE
- Ch 3Build multi-architecture images for GKE
- Ch 4Install ArgoCD and deploy first application