Skip to content

Instantly share code, notes, and snippets.

@palewire
Last active April 15, 2024 20:38
Show Gist options
  • Star 83 You must be signed in to star a gist
  • Fork 23 You must be signed in to fork a gist
  • Save palewire/12c4b2b974ef735d22da7493cf7f4d37 to your computer and use it in GitHub Desktop.
Save palewire/12c4b2b974ef735d22da7493cf7f4d37 to your computer and use it in GitHub Desktop.
How to push tagged Docker releases to Google Artifact Registry with a GitHub Action

How to push tagged Docker releases to Google Artifact Registry with a GitHub Action

Here's how I configured a GitHub Action so that a new version issued by GitHub's release interface will build a Dockerfile, tag it with the version number and upload it to Google Artifact Registry.

Before you attempt the steps below, you need the following:

  • A GitHub repository that contains a working Dockerfile
  • The Google Cloud SDK tool gcloud installed and authenticated

Create a Workload Identity Federation

The first step is to create a Workload Identity Federation that will allow your GitHub Action to log in to your Google Cloud account. The instructions below are cribbed from the documentation for the google-github-actions/auth Action. You should follow along in your terminal.

The first command creates a service account with Google. I will save the name I make up, as well as my Google project id, as environment variables for reuse. You should adapt the variables here, and others as we continue, to fit your project and preferred naming conventions.

export PROJECT_ID=my-project-id
export SERVICE_ACCOUNT=my-service-account

gcloud iam service-accounts create "${SERVICE_ACCOUNT}" \
  --project "${PROJECT_ID}"

Enable Google's IAM API for use.

gcloud services enable iamcredentials.googleapis.com \
  --project "${PROJECT_ID}"

Create a workload identity pool that will manage the GitHub Action's roles in Google Cloud's permission system.

export WORKLOAD_IDENTITY_POOL=my-pool

gcloud iam workload-identity-pools create "${WORKLOAD_IDENTITY_POOL}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="${WORKLOAD_IDENTITY_POOL}"

Get the unique identifier of that pool.

gcloud iam workload-identity-pools describe "${WORKLOAD_IDENTITY_POOL}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)"

Export the returned value to a new variable.

export WORKLOAD_IDENTITY_POOL_ID=whatever-you-got-back

Create a provider within the pool for GitHub to access.

export WORKLOAD_PROVIDER=my-provider

gcloud iam workload-identity-pools providers create-oidc "${WORKLOAD_PROVIDER}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="${WORKLOAD_IDENTITY_POOL}" \
  --display-name="${WORKLOAD_PROVIDER}" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"

Allow a GitHub Action based in your repository to login to the service account via the provider.

export REPO=my-username/my-repo

gcloud iam service-accounts add-iam-policy-binding "${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

Ask Google to return the identifier of that provider.

gcloud iam workload-identity-pools providers describe "${WORKLOAD_PROVIDER}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="${WORKLOAD_IDENTITY_POOL}" \
  --format="value(name)"

That will return a string that you should save for later. We'll use it in our GitHub Action.

Finally, we need to make sure that the service account we created at the start has permission to muck around with Google Artifact Registry.

gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
    --role="roles/artifactregistry.admin"

To verify that worked, you can ask Google print out the permissions assigned to the service account.

gcloud projects get-iam-policy $PROJECT_ID \
    --flatten="bindings[].members" \
    --format='table(bindings.role)' \
    --filter="bindings.members:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com"

Create a Google Artifact Registry repository

Go to the Google Artifact Registry interface within your project. Create a new repository by hitting the buttona at the top. Tell Google it will be in the Docker format and then select a region. It doesn't matter which region. Save the name you give the repo and the region's abbreviation, which will be something like us-west1.

Create a GitHub Action

Now it's time to make your GitHub Action. You should add a new YAML file in the .github/workflows folder. In the authentication step you'll want to fill in your provider id, your service account id and project id. In the push step you'll need to fill in your GAR repository name and region, as well as a name for your image, which you'll need to make up on your own.

name: Release
on:
  push:

jobs:
  docker-release:
    name: Tagged Docker release to Google Artifact Registry
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')  # <-- Notice that I'm filtering here to only run when a tagged commit is pushed

    permissions:
      contents: 'read'
      id-token: 'write'

    steps:
      - id: checkout
        name: Checkout
        uses: actions/checkout@v2

      - id: auth
        name: Authenticate with Google Cloud
        uses: google-github-actions/auth@v0
        with:
          token_format: access_token
          workload_identity_provider: <your-provider-id>
          service_account: <your-service-account>@<your-project-id>.iam.gserviceaccount.com
          access_token_lifetime: 300s

      - name: Login to Artifact Registry
        uses: docker/login-action@v1
        with:
          registry: us-west2-docker.pkg.dev
          username: oauth2accesstoken
          password: ${{ steps.auth.outputs.access_token }}

      - name: Get tag
        id: get-tag
        run: echo ::set-output name=short_ref::${GITHUB_REF#refs/*/}

      - id: docker-push-tagged
        name: Tag Docker image and push to Google Artifact Registry
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: |
             <your-gar-region>-docker.pkg.dev/<your-project-id>/<your-gar-repo-name>/<your-docker-image-name>:${{ steps.get-tag.outputs.short_ref }}
             <your-gar-region>-docker.pkg.dev/<your-project-id>/<your-gar-repo-name>/<your-docker-image-name>:latest

Make a release

Phew. That's it. Not you can go to the releases panel for your repo on GitHub, punch in a new version tag like 0.0.1 and hit the big green button. That should trigger a new process in your Actions tab, where the push of the tagged commit will trigger the release.

Extra bits

In my real world implementations, I will typically have several testing steps that precede the release job. That will ensure that the code is good to go before sending out the release.

That could look something like this:

name: Tesst and release
on:
  push:

jobs:
  test-python:
    name: Test Python code
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - id: install
        name: Install Python, pipenv and Pipfile packages
        uses: palewire/install-python-pipenv-pipfile@v2
        with:
          python-version: 3.7

      - id: run
        name: Run tests
        run: make test

  docker-release:
    name: Tagged Docker release to Google Artifact Registry
    runs-on: ubuntu-latest
    needs: [test-python]
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')

    permissions:
      contents: 'read'
      id-token: 'write'

    steps:
      - id: checkout
        name: Checkout
        uses: actions/checkout@v2

      - id: auth
        name: Authenticate with Google Cloud
        uses: google-github-actions/auth@v0
        with:
          token_format: access_token
          workload_identity_provider: <your-provider-id?
          service_account: <your-service-account>@<your-project-id>.iam.gserviceaccount.com
          access_token_lifetime: 300s

      - name: Login to Artifact Registry
        uses: docker/login-action@v1
        with:
          registry: us-west2-docker.pkg.dev
          username: oauth2accesstoken
          password: ${{ steps.auth.outputs.access_token }}

      - name: Get tag
        id: get-tag
        run: echo ::set-output name=short_ref::${GITHUB_REF#refs/*/}

      - id: docker-push-tagged
        name: Tag Docker image and push to Google Artifact Registry
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: |
             <your-gar-region>-docker.pkg.dev/<your-project-id>/<your-gar-repo-name>/<your-docker-image-name>:${{ steps.get-tag.outputs.short_ref }}
             <your-gar-region>-docker.pkg.dev/<your-project-id>/<your-gar-repo-name>/<your-docker-image-name>:latest

If you just wanted tags to trigger releases, you could likely hook the action to run on tags rather than on every push. That would mean putting something like this at the top, and removing the if clause I've put on the release job to filter out typical pushes.

on:  
  push:
    tags:
      - '*'

But all that's up to you. Good luck. It's a real pain to get all these ducks in a row, but once you do it you'll have a streamlined release system that can be repeated quickly and smoothly well into the future.

@UmungoBungo
Copy link

legendary 🦸

@bmritz
Copy link

bmritz commented May 12, 2023

Thank you. You are a professional. The commands all worked straight through.

@zamai
Copy link

zamai commented May 18, 2023

After reading hours of OpenID Connect docs - this was a breath of fresh air! thanks!

@antham
Copy link

antham commented Jun 14, 2023

The terraform counterpart to setup the credentials

data "google_project" "project" {
}

resource "google_artifact_registry_repository" "<repository_name>" {
  location      = "<location>"
  repository_id = "<repository_name>"
  description   = "<description>"
  format        = "DOCKER"
}

resource "google_service_account_iam_binding" "<repository>-repository-iam" {
  service_account_id = google_service_account.github.name
  role               = "roles/iam.workloadIdentityUser"

  members = [
    "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/<github_org>/<github_repository_name>"
  ]
}

resource "google_service_account" "github" {
  account_id   = "github"
  project      = google_project.project.project_id
  display_name = "GitHub"
}

resource "google_iam_workload_identity_pool" "github" {
  workload_identity_pool_id = "github"
}

resource "google_iam_workload_identity_pool_provider" "github" {
  project                            = google_project.core_project.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id
  workload_identity_pool_provider_id = "github"
  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.actor"      = "assertion.actor"
    "attribute.repository" = "assertion.repository"
  }
  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

resource "google_project_iam_binding" "read-write-registry-iam" {
  project = google_project.core_project.project_id
  role    = "roles/artifactregistry.writer"
  members = ["serviceAccount:${google_service_account.github.email}"]
}

@aleenprd
Copy link

Hello! Really cool guide, yet I am facing an issue and maybe someone here can help me figure this out. Here is my .yml for the action.

docker:
   # needs: test
   runs-on: ubuntu-latest
   timeout-minutes: 5
   permissions:
     contents: 'read'
     id-token: 'write'
   steps:
     -
       name: Checkout
       uses: actions/checkout@v3
     -
       name: Set up QEMU
       uses: docker/setup-qemu-action@v2
     -
       name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v2
       with:
         driver-opts: image=moby/buildkit:master
     -
       name: Login to Docker Hub
       uses: docker/login-action@v2
       with:
         username: ${{ secrets.DOCKERHUB_USERNAME }}
         password: ${{ secrets.DOCKERHUB_TOKEN }}
     - 
       id: authgcp
       name: Authenticate to Google Cloud
       uses: google-github-actions/auth@v1
       with:
         token_format: access_token
         workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER_RESOURCE_NAME }}
         service_account: ${{ secrets.SERVICE_ACCOUNT }}@${{ secrets.PROJECT_ID }}.iam.gserviceaccount.com
         access_token_lifetime: 600s
     - 
       name: Login to Artifact Registry
       uses: docker/login-action@v2
       with:
         registry: ${{ secrets.GAR_REGION }}-docker.pkg.dev
         username: oauth2accesstoken
         password: ${{ steps.authgcp.outputs.access_token }}
     -
       name: Build and push
       uses: docker/build-push-action@v4
       with:
         context: .
         push: true
         tags: |
           ${{ secrets.DOCKERHUB_USERNAME }}/${{ vars.APP_NAME }}:latest
           ${{ secrets.GAR_REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/${{ secrets.GAR_REPO }}/${{ vars.APP_NAME }}:latest

The error I am facing is:

ERROR: failed to solve: failed to push ***-docker.pkg.dev/***/***/homer:latest: unexpected status: 400 Bad Request
Error: buildx failed with: ERROR: failed to solve: failed to push ***-docker.pkg.dev/***/***/homer:latest: unexpected status: 400 Bad Request

@antham
Copy link

antham commented Jun 17, 2023

What did you do in the previous steps to setup the registry ?

@victorboissiere
Copy link

Very useful, thanks a lot!

@sphtd
Copy link

sphtd commented Jul 3, 2023

ERROR: denied: Artifact Registry API has not been used in project before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/artifactregistry.googleapis.com/overview?project= then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
Error: buildx failed with: ERROR: denied: Artifact Registry API has not been used in project before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/artifactregistry.googleapis.com/overview?project= then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry

Anyone?

@palewire
Copy link
Author

palewire commented Jul 3, 2023

ERROR: denied: Artifact Registry API has not been used in project before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/artifactregistry.googleapis.com/overview?project= then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. Error: buildx failed with: ERROR: denied: Artifact Registry API has not been used in project before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/artifactregistry.googleapis.com/overview?project= then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry

Anyone?

Make sure that you first enable Google's IAM API for use, before everything else that follows

gcloud services enable iamcredentials.googleapis.com \
  --project "${PROJECT_ID}"

@isurus95
Copy link

isurus95 commented Aug 5, 2023

superb.
There's a static region specified here,

name: Login to Artifact Registry
        uses: docker/login-action@v1
        with:
          registry: us-west2-docker.pkg.dev

better if the region can be parameterized too.
Like this

name: Login to Artifact Registry
        uses: docker/login-action@v1
        with:
          registry: <your-gar-region>-docker.pkg.dev

@Goldziher
Copy link

fantastic, very helpful!

@esgn
Copy link

esgn commented Mar 26, 2024

Thanks for the tutorial.
I had to change --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" to --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" to make it work. In https://cloud.google.com/blog/products/identity-security/enabling-keyless-authentication-from-github-actions?hl=en the same attribute mapping is suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment