DEV Community

Cover image for Push Images To Artifact Registry Using GitHub Actions & Workload Identity
Filip Lindqvist
Filip Lindqvist

Posted on • Edited on

3 2 1 1 3

Push Images To Artifact Registry Using GitHub Actions & Workload Identity

Day-to-day, I mainly contribute to Elva, a powerhouse of AWS experts aiming to help our customers elevate their cloud journeys using serverless.

However, sometimes life brings you into other platforms like Google Cloud in this case (which I also like a lot). This time, I wanted to set up container deploys as a part of a GitHub Action workflow to push images to Google Cloud Artifact Registry.

In the past, we primarily established our service-to-service (GitHub-to-Google-Cloud) authentication using service accounts with key files. While service account keys are powerful tools, these long-lived keys can present security challenges if not handled with care. The modern way of doing this is using Workload Identity Federation, which makes it easier and more secure to use Identity and Access Management (IAM) to grant external identities the necessary IAM roles and rights.

Let's look at how to set up the workflow file and the necessary Google Cloud configurations to achieve this.

Step 0: Set Up Workload Identity Federation in Google Cloud

Before we even touch the GitHub Actions YAML, we need to configure Google Cloud to trust our GitHub Actions workflow. This will be a secure way to allow our GitHub Actions to access Google Cloud resources without needing to manage long-lived service account keys.

Here's a script that sets up everything you need.

Important:

  • Make sure you have the gcloud CLI installed and configured.
    • Otherwise you can install: brew install google-cloud-sdk
  • You'll need to replace placeholder values in the script.
#!/bin/bash

# 1. Setup
# --- Input Variables (UPDATE THESE VALUES!) ---
GCLOUD_PROJECT="your-gcp-project-id"
GITHUB_REPO="your-github-username/your-github-repo-name"

# --- Optional ---
# Name for the new service account, identity pool and identity provider, change if you want/need
GCLOUD_SERVICE_ACCOUNT="github-deployer-account"
GCLOUD_IDENTITY_POOL="github-deployer-auth-pool"
GCLOUD_IDENTITY_PROVIDER="github-deployer-auth-provider"

GCLOUD_SERVICE_ACCOUNT_EMAIL="${GCLOUD_SERVICE_ACCOUNT}@${GCLOUD_PROJECT}.iam.gserviceaccount.com"

echo "Starting Workload Identity Federation setup for \
 project: ${GCLOUD_PROJECT} and repo: ${GITHUB_REPO}"

# 2. Enable the IAM Credentials API
gcloud services enable iamcredentials.googleapis.com \
  --project "${GCLOUD_PROJECT}"
if [ $? -ne 0 ]; then echo "Error enabling IAM Credentials API. Exiting."; exit 1; fi

# 3. Create a new service account
gcloud iam service-accounts create ${GCLOUD_SERVICE_ACCOUNT} \
  --project "${GCLOUD_PROJECT}" \
  --display-name="GitHub Actions Deployer Account for ${GITHUB_REPO}"

# 4. Grant the service account permission to write to Artifact Registry
gcloud projects add-iam-policy-binding "${GCLOUD_PROJECT}" \
    --member="serviceAccount:${GCLOUD_SERVICE_ACCOUNT_EMAIL}" \
    --role="roles/artifactregistry.writer"
if [ $? -ne 0 ]; then echo "Error granting Artifact Registry Writer role. Exiting."; exit 1; fi

# 5. Create a Workload Identity Pool
gcloud iam workload-identity-pools create ${GCLOUD_IDENTITY_POOL} \
  --project="${GCLOUD_PROJECT}" \
  --location="global" \
  --display-name="GitHub Actions Auth Pool"

# 6. Get the full ID of the Workload Identity Pool
GCLOUD_WORKLOAD_IDENTITY_POOL_ID=$(gcloud iam workload-identity-pools describe ${GCLOUD_IDENTITY_POOL} \
  --project="${GCLOUD_PROJECT}" \
  --location="global" \
  --format="value(name)")
if [ -z "${GCLOUD_WORKLOAD_IDENTITY_POOL_ID}" ]; then echo "Error fetching Workload Identity Pool ID. Exiting."; exit 1; fi

# 7. Create an OIDC Workload Identity Provider in the pool..."
gcloud iam workload-identity-pools providers create-oidc ${GCLOUD_IDENTITY_PROVIDER} \
  --project="${GCLOUD_PROJECT}" \
  --location="global" \
  --workload-identity-pool="${GCLOUD_IDENTITY_POOL}" \
  --display-name="GitHub Actions Auth Provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"

# 8. Allow authentications from the GitHub OIDC provider to impersonate the Google Cloud service account
# This binds the GitHub repo to the service account via the WIF pool and provider.
gcloud iam service-accounts add-iam-policy-binding "${GCLOUD_SERVICE_ACCOUNT_EMAIL}" \
  --project="${GCLOUD_PROJECT}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${GCLOUD_WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${GITHUB_REPO}"
if [ $? -ne 0 ]; then echo "Error binding service account for impersonation. Exiting."; exit 1; fi

# 9. Get the full ID of the Workload Identity Provider (needed for GitHub Secrets)
GCP_WORKLOAD_IDENTITY_PROVIDER_ID=$(gcloud iam workload-identity-pools providers describe ${GCLOUD_IDENTITY_PROVIDER} \
    --project="${GCLOUD_PROJECT}" \
    --location="global" \
    --workload-identity-pool="${GCLOUD_IDENTITY_POOL}" \
    --format="value(name)")
if [ -z "${GCP_WORKLOAD_IDENTITY_PROVIDER_ID}" ]; then echo "Error fetching Workload Identity Provider ID. Exiting."; exit 1; fi

echo ""
echo "--- GitHub Secrets ---"
echo "Add these to your GitHub repository secrets:"
echo "GCP_PROJECT_ID: ${GCLOUD_PROJECT}"
echo "GCP_WORKLOAD_IDENTITY_PROVIDER_ID: ${GCP_WORKLOAD_IDENTITY_PROVIDER_ID}"
echo "GCP_SERVICE_ACCOUNT_EMAIL: ${GCLOUD_SERVICE_ACCOUNT_EMAIL}"
echo ""
echo "Setup complete!"

Enter fullscreen mode Exit fullscreen mode

What this script does:

  1. Setup
    • GCLOUD_PROJECT: You must set this to your Google Cloud Project ID.
    • GITHUB_REPO: You must set this to your GitHub repository in the format username/repository-name (e.g., my-cool-user/my-awesome-app).
  2. Enables IAM Credentials API
    • This API is necessary for your GitHub Action to mint short-lived credentials for the service account.
  3. Creates a Service Account
    • This is the Google Cloud identity that your GitHub Action will impersonate. It's like a robot user for your automation.
  4. Grants Artifact Registry Permissions
    • This gives the newly created service account permission to push images to Google Cloud Artifact Registry.
  5. Creates a Workload Identity Pool
    • This pool is a container for identity providers. It helps organize how external identities (like those from GitHub) are managed.
  6. Gets the Workload Identity Pool ID
    • This retrieves the unique identifier for the pool, which is needed in later steps.
  7. Creates an OIDC Workload Identity Provider
    • This is the key part. It tells Google Cloud to trust OIDC tokens issued by GitHub Actions (https://token.actions.githubusercontent.com).
    • The attribute-mapping tells Google Cloud how to map information from the GitHub OIDC token (like the repository name) to attributes Google Cloud can understand. This allows you to restrict which GitHub repositories can use this identity provider.
  8. Allows GitHub to Impersonate the Service Account
    • This crucial step links everything together. It says that principals (in this case, your GitHub Action running in the specified GITHUB_REPO) that authenticate through the OIDC provider are allowed to impersonate the service account we created. The principalSet specifically targets your repository.
  9. Outputs IDs for GitHub Secrets
    • The script will print out the GCP_PROJECT_ID, GCP_WORKLOAD_IDENTITY_PROVIDER_ID (this will be the full path like projects/your-gcp-project-id/locations/global/workloadIdentityPools/github-deployer-auth-pool/providers/github-deployer-auth-provider), and GCP_SERVICE_ACCOUNT_EMAIL. You must add these as secrets in your GitHub repository settings so the Action workflow can use them. You do this by going to your Github repo, then Settings > Secrets and variables > Actions > New repository secret.

After running this script successfully and setting up the GitHub secrets, Google Cloud will be ready for your GitHub Action.

Now, let's look at the GitHub Actions workflow YAML.

name: Build Container

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    # Permissions are important for the worklaod identity federation to work
    permissions:
      contents: 'read'
      id-token: 'write'

    steps:
    # ... our steps will go here
Enter fullscreen mode Exit fullscreen mode

This initial block sets up the basic triggers (when the workflow runs) and the environment (Ubuntu latest) for our job.
It will run when we push to the main branch or when a pull request targets the main branch. The job, which we've called build, will run on the latest Ubuntu environment.

Notice the permissions block. We need contents: 'read' to allow the action to read your repository's content (which actions/checkout does). More importantly, id-token: 'write' is crucial. This permission allows the GitHub Actions runner to request an OIDC (OpenID Connect) token, which we'll use to securely authenticate with Google Cloud, thanks to the Workload Identity Federation setup we just did.

Now for the actual steps involved in the job.

Step 1: Checkout Code And Set Up Docker

First things first, we need to get our code and prepare Docker for building our image.

    - uses: actions/checkout@v4
    - uses: docker/setup-buildx-action@v3
Enter fullscreen mode Exit fullscreen mode

This initial part combines two actions:

  1. actions/checkout@v4: This action checks out your repository.
  2. docker/setup-buildx-action@v3: This action installs and configures Docker.

Step 2: Authenticate to Google Cloud

To push our Docker image to Google Cloud Artifact Registry, our GitHub Action needs to authenticate with Google Cloud.
This step uses the Workload Identity Federation we configured in Step 0.

    - uses: 'google-github-actions/auth@v2'
      id: auth
      with:
        token_format: access_token
        project_id: ${{ secrets.GCP_PROJECT_ID }}
        workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER_ID }}
        service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
Enter fullscreen mode Exit fullscreen mode

Let's break down the parameters:

  • auth: We give this step an ID, auth. This is useful because this action outputs an access token that we can use in subsequent steps.
  • token_format: access_token: We're asking for an OAuth2 access token.
  • project_id: Your Google Cloud Project ID. This comes from the GCP_PROJECT_ID secret you created from the script's output.
  • workload_identity_provider: The full identifier of your Workload Identity Provider. This comes from the WORKLOAD_IDENTITY_PROVIDER_ID secret.
  • service_account: The email address of the Google Cloud service account that GitHub Actions will impersonate. This comes from the GCP_SERVICE_ACCOUNT_EMAIL secret.

Step 3: Log in to Google Cloud Artifact Registry

Now that we have an access token from Google Cloud, we can use it to log in to Artifact Registry.
Artifact Registry uses standard Docker authentication mechanisms.

    - uses: docker/login-action@v3
      with:
        registry: europe-docker.pkg.dev
        username: oauth2accesstoken
        password: '${{ steps.auth.outputs.access_token }}'

Enter fullscreen mode Exit fullscreen mode

Let's break down the parameters:

  • registry: This is the hostname of the Artifact Registry you want to push to. For example, if your images are in the europe region, it would be europe-docker.pkg.dev. Adjust this based on your registry's region.
  • username: When using an OAuth2 access token with Google Artifact Registry, the username is always oauth2accesstoken.
  • password: This is where we use the access token obtained in the previous step. ${{ steps.auth.outputs.access_token }} refers to the output named access_token from the step with the ID auth.

Step 4: Build and Push Docker Image

Finally, we get to the part where we build our Docker image and push it.

    - name: Build and push Docker image
      uses: docker/build-push-action@v6
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: ${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }}
        tags: europe-docker.pkg.dev/${{secrets.GCP_PROJECT_ID }}/images/my-container-name:latest
Enter fullscreen mode Exit fullscreen mode

Let's break down the parameters:

  • platforms: linux/amd64,linux/arm64: This is an example of building for multiple platforms. Buildx makes this easy. You can adjust this to the platforms you need.
  • push: ${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }}: This is a crucial part for controlling when the image is actually pushed. We only want to push if the current Git reference is refs/heads/main (i.e., we're on the main branch) AND the event that triggered the workflow is not a pull_request. This prevents pushing images for every commit on a pull request branch, but still builds them (which can be useful for testing).
  • tags: This specifies the tag for your Docker image.

Optimizing Builds with Caching

Docker builds can sometimes be slow, especially if you have a large image or many dependencies. Caching can significantly speed up this process by reusing layers from previous builds. The docker/build-push-action supports caching with GitHub Actions cache.

Let's update our "Build and push Docker image" step to include caching:

    - name: Build and push Docker image
      uses: docker/build-push-action@v6
      with:
        # ... add these lines
        cache-from: type=gha
        cache-to: type=gha,mode=max
Enter fullscreen mode Exit fullscreen mode

Here's what the new lines mean:

  • cache-from: type=gha: This tells the action to attempt to pull cache layers from the GitHub Actions cache.
  • cache-to: type=gha,mode=max: This tells the action to save the build cache to the GitHub Actions cache. mode=max means it will include all layers, which provides the best potential for cache hits on subsequent runs, though it might make the cache slightly larger.

Using type=gha is convenient because it uses the built-in GitHub Actions caching mechanism.

Wrapping Up: The Complete Workflow

So, putting it all together, here's our final YAML for the GitHub Action:

name: Build Container

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

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

    steps:
    - uses: actions/checkout@v4
    - uses: docker/setup-buildx-action@v3

    - uses: 'google-github-actions/auth@v2'
      id: auth
      with:
        token_format: access_token
        project_id: ${{secrets.GCP_PROJECT_ID }}
        workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER_ID }}
        service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}

    - uses: docker/login-action@v3
      with:
        registry: europe-docker.pkg.dev
        username: oauth2accesstoken
        password: '${{ steps.auth.outputs.access_token }}'

    - name: Build and push Docker image
      uses: docker/build-push-action@v6
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: ${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }}
        tags: europe-docker.pkg.dev/${{secrets.GCP_PROJECT_ID }}/images/my-container-name:latest
        cache-from: type=gha
        cache-to: type=gha,mode=max
Enter fullscreen mode Exit fullscreen mode

And there you have it!

First, ensure you've run the setup script (Step 0) in your Google Cloud environment and configured the necessary GitHub secrets.

Then, this workflow will check out your code, authenticate to Google Cloud, log in to Artifact Registry, and then build and push your Docker image, complete with caching to speed things up.

Remember to replace placeholder values like europe-docker.pkg.dev and my-container-name with your own values.
You'll also need to ensure your Dockerfile is in the root of your repository (context: .) or adjust the path accordingly.

This setup provides a secure and efficient way to manage your Docker image deployments to Google Cloud.

Top comments (0)