How I Push Container Images to Google Artifact Registry with GitHub Actions and Workload Identity Federation
Ever since GitHub announced Actions in 2018, it has been my go-to CI tool for personal projects. I will say, I have been tempted at times to switch to other tools, such as Argo Workflows, but with GitHub Actions, there is native support within my GitHub repositories and I don’t have to host anything.
One switch I recently have made is to Google Artifact Registry from DockerHub to host my container images. With the combination of GitHub Actions, Artifact Registry, and Workload Identity Federation, I am able to push my images into a private registry without having to generate and store any long lived credentials.
The core of my GCP environment is built and maintained within a single repository containing all the Terraform code needed to run my environment. Given that, the steps shown here were orchestrated through that repository as well. If you are interested in doing this through a CLI, I recommend you check out this GitHub Gist.
In this blog, I’ll walk through how I set up my application’s GitHub repository (found here) to automatically build and push container images to Google Artifact Registry with Workload Identity Federation.
Note: It is important to understand Workload Identity Federation prior to using it. A good initial resource can be found in the documentation here.
Setting up Google Artifact Registry
Setting up the Artifact Registry was pretty elementary when doing it with Terraform since I leveraged the Google Cloud Foundation Fabric modules.
The Terraform code below specifies the project, region, format, and name of the registry. I also leveraged the module to assign the Artifact Registry Admin role to the admin group in my organization.
module "docker_artifact_registry" {
source = "github.com/GoogleCloudPlatform/cloud-foundation-fabric.git//modules/artifact-registry?ref=v20.0.0"
project_id = module.project.project_id
location = var.region
format = "DOCKER"
id = "core"
iam = {
"roles/artifactregistry.admin" = [format("group:%s", var.admin_group)]
}
}
Setting up Workload Identity Federation
Setting up Workload Identity Federation required two steps. First I created a dedicated service account that will be used by the GitHub Actions workflow to authenticate to Google Cloud and push the built image to Artifact Registry.
The Terraform code below creates the service account and assigns the predefined Artifact Registry Writer role.
module "wif-artifact-registry-service-account" {
source = "github.com/GoogleCloudPlatform/cloud-foundation-fabric.git//modules/iam-service-account?ref=v20.0.0"
project_id = module.project.project_id
name = "artifact-registry-pusher"
description = "Artifact Registry Pusher WIF service account"
generate_key = false
iam_project_roles = {
(module.project.project_id) = [
"roles/artifactregistry.writer",
]
}
}
The second step was to add the newly created service account to the Workload Identity pool. The Terraform code below creates a dedicated workload identity pool with the GitHub provider and adds the service account to the pool. Additionally, I assigned an attribute to the mapping to say that this service account must only be used in the specified GitHub repository here. With this assignment, if I were to try to use this service account in a different repository, authentication would fail.
module "gh_oidc" {
source = "terraform-google-modules/github-actions-runners/google//modules/gh-oidc"
project_id = module.project.project_id
pool_id = "github-pool-prod"
provider_id = "github-provider-prod"
sa_mapping = {
...
"artifact-registry-account" = {
sa_name = module.wif-artifact-registry-service-account.id
attribute = format("attribute.repository/%s/scoreboard", var.github_organization)
}
}
}
Crafting the GitHub Actions
With Google Cloud configured, the last piece was to set up the GitHub Actions. I currently have two actions in the repository. The first one (found here) runs tests and scans the container image for any vulnerabilities and is run anytime I merge code into the main branch.
The second action, which is the one I am focusing on here, builds and pushes the image to Artifact Registry and only runs when I create a new release.
It contains five steps:
- Checkout the code in the repository.
- Get the current release tag. This was used to tag the container image before pushing to Artifact Registry.
- Authenticate to Google Cloud with the specified service account and workload identity provider. I also specified that I want the token format as an
access_token
to use to authenticate to Artifact Registry in the next step. - Login to Artifact Registry with the
access_token
. - Build and tag the container image with both the release and latest tag and then push it to Artifact Registry named
core
in my main GCP projectproj-mission-control-80492
.
Note: I redacted some text in the below snippet for eligibility. Please see the repository for the complete code.
name: Build and Push
on:
push:
tags: 'v*.*.*'
jobs:
build-and-push:
name: Build and Push
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get tag
id: get-tag
run: echo ::set-output name=short_ref::${GITHUB_REF#refs/*/}
- name: Authenticate to Google Cloud
id: auth-gcp
uses: google-github-actions/auth@v0
with:
token_format: access_token
workload_identity_provider: ...
service_account: ...
- name: Login to Artifact Registry
uses: docker/login-action@v1
with:
registry: us-east1-docker.pkg.dev
username: oauth2accesstoken
password: ${{ steps.auth-gcp.outputs.access_token }}
- name: Tag Docker image and push to Google Artifact Registry
id: build-push-tag
uses: docker/build-push-action@v2
with:
push: true
tags: |
us-east1-docker.pkg.dev/proj-mission-control-80492/core/scoreboard:${{ steps.get-tag.outputs.short_ref }}
us-east1-docker.pkg.dev/proj-mission-control-80492/core/scoreboard:latest
Wrapping Up
With the GitHub Actions in place, the repository is now equipped to build and push the container images for my application whenever I cut a new release. This has significantly increased the speed at which I can release new updates of my application since I just need to worry about the code. Testing, scanning, and pushing the container is now taken care of securely by GitHub Actions and Workload Identity Federation.