CI/CD Pipeline with Github Actions to deploy to Cloud Run


Deploying applications quickly and reliably is crucial for any development team. In this article, I’ll show how I set up a CI/CD pipeline using GitHub Actions to automate the deployment of an application to Google Cloud Run.

There are 5 steps to it:

  1. (Essentials and GCP Setup)
  2. Checkout Repo
  3. Authenticate in Google Cloud
  4. Docker Login (Artifact Registry)
  5. Build and Push Image
  6. Deploy to Cloud Run

Below is a step-by-step explanation of all steps, using a Golang backend project of mine as an example. At the end you can find the full workflow.

Essentials and GCP Setup

Before getting started, there are a few things that need to be set up in GCP in order to proceed.

First, create a service account (IAM & Admin > Service Accounts) for the action and add the roles:

  1. Artifact Registry Writer (To upload images to Artifact Registry)
  2. Cloud Run Developer (To deploy service with the new image)
  3. Service Account User (To let this account be impersonated by WIF)
  4. Secret Manager Secret Accessor (In case your service needs to access secrets)

Afterward, in IAM & Admin > Workload Identity Federation create an identity pool, connect to an identity provider, configure the provider mapping and grant access to selected resources. This video explains it better than I can and is very straightforward.

Next, make sure you have a Docker Repo set up in Artifact Registry. This is a simple step, just make sure that the format is Docker and take note of the region.

And finally, create a service in Cloud Run. Make sure to set the service account in the Security tab to the one previously created with those roles.

Regarding the workflow itself, there are of course the essentials such as the workflow name, trigger, jobs, runner and relevant environment variables.

Make sure you have access to the following variables in some manner, be it hardcoded in the script or Github variables/secrets according to your needs.

REPO: Name of the docker repository in artifact registry.
REGION: Region of the Cloud Run Service.
WIF_PROVIDER: Audience in WIF Provider.

Looks like https://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_NAME/providers/PROVIDER_NAME

SERVICE_ACCOUNT: Email of the service account
GAR_LOCATION: Region of the Artifact Registry (could be condensed into REGION)
PROJECT_ID: Project ID in Google Cloud SERVICE: Cloud Run Service name. I set this programmatically depending on the branch, see here

# 0. Workflow essentials
name: Go Backend to Cloud Run

on:
  push:
    branches:
      - master
      - dev
    paths:
      - "server/cmd/server/main.go"
      - "server/internal/**"
      - "server/services/**"
      - "server/go.mod"
      - "server/go.sum"
      - "server/Dockerfile.gcp"
      - "!server/**/**test.go"

env:
  REPO: go-repo
  REGION: europe-west3

  WIF_PROVIDER: ${{ secrets.WIF_PROVIDER }}
  SERVICE_ACCOUNT: ${{ secrets.WIF_SERVICE_ACCOUNT }}
  GAR_LOCATION: ${{ vars.GAR_LOCATION }}
  PROJECT_ID: ${{ vars.PROJECT_ID }}

jobs:
  deploy:
    permissions:
      contents: "read"
      id-token: "write"

    runs-on: ubuntu-latest
	  steps:
		  ...

Checkout Repo

The first step is checkout the repository so the workflow can access it.

# 1. Checkout
jobs:
  deploy:
	...
    steps:
		- name: Checkout
   		  uses: actions/checkout@v4

Authenticate in Google Cloud

Next you need to authenticate in Google Cloud using the Service Account and and WIF Provider.

# 2. Google Cloud Auth
jobs:
  deploy:
	...
    steps:
	  ...

      - name: Google Auth
        id: auth
        uses: "google-github-actions/auth@v2"
        with:
          token_format: "access_token"
          workload_identity_provider: "${{ secrets.WIF_PROVIDER }}"
          service_account: "${{ secrets.WIF_SERVICE_ACCOUNT }}"

Docker Login (Artifact Registry)

Artifact Registry also has its own auth step since it’s a Docker repo. Use the token from the previous step here.

# 3. Docker Repo/Artifact Registry Auth
jobs:
  deploy:
	...
    steps:
	  ...

      - name: Docker Auth
        id: docker-auth
        uses: "docker/login-action@v3"
        with:
          username: "oauth2accesstoken"
          password: "${{ steps.auth.outputs.access_token }}"
          registry: "${{ vars.GAR_LOCATION }}-docker.pkg.dev"

Build and Push Image

Build and Push the Image to Artifact Registry. This could also be done in GCP using their solution for builds but I find this approach simpler. The image must have the correct tag format in order for the push to work correctly. The last path segment is up to you, I use the prefix go_ with the commit sha.

# 4. Build and Push Image to Artifact Registry
jobs:
  deploy:
	...
    steps:
	  ...

      - name: Build and Push Image
        run: |-
          docker build -t "${{ vars.GAR_LOCATION }}-docker.pkg.dev/${{ vars.PROJECT_ID }}/${{ env.REPO }}/go_${{ github.sha }}" -f server/Dockerfile.gcp ./server
          docker push "${{ vars.GAR_LOCATION }}-docker.pkg.dev/${{ vars.PROJECT_ID }}/${{ env.REPO }}/go_${{ github.sha }}"          

OPTIONAL

In my use case, I use the same workflow for a private development service and a public production one. Based on the current branch, I set the SERVICE variable to either the dev or prod service’s name.

# OPTIONAL Set Service name
jobs:
  deploy:
	...
    steps:
	  ...

      - name: Set service name and Dockerfile
        id: set-variables
        run: |-
          if [ "${{ github.ref }}" == "refs/heads/dev" ]; then
            echo "SERVICE=dev-go-app" >> $GITHUB_ENV
          elif [ "${{ github.ref }}" == "refs/heads/master" ]; then
            echo "SERVICE=go-app" >> $GITHUB_ENV
          fi          

Deploy to Cloud Run

Deploy to Cloud Run, using the SERVICE variable as a target.

# 5. Deploy to Cloud Run
jobs:
  deploy:
	...
    steps:
	  ...

      - name: Deploy to Cloud Run
        id: deploy
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: ${{ env.SERVICE }}
          region: ${{ env.REGION }}
          image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO }}/go_${{ github.sha }}

Full Workflow

And that’s it. Below you can find the full script. It’s actually pretty straightforward, my biggest pain point was in the initial setup, having to work out the whole Workload Identity Federation service, it can be complicated but the video provided by Google themselves serves as a nice walkthrough.

name: Go Backend to Cloud Run

on:
  push:
    branches:
      - master
      - dev
    paths:
      - "server/cmd/server/main.go"
      - "server/internal/**"
      - "server/services/**"
      - "server/go.mod"
      - "server/go.sum"
      - "server/Dockerfile.gcp"
      - "!server/**/**test.go"

env:
  REPO: go-repo
  REGION: europe-west3

  WIF_PROVIDER: ${{ secrets.WIF_PROVIDER }}
  SERVICE_ACCOUNT: ${{ secrets.WIF_SERVICE_ACCOUNT }}
  GAR_LOCATION: ${{ vars.GAR_LOCATION }}
  PROJECT_ID: ${{ vars.PROJECT_ID }}

jobs:
  deploy:
    permissions:
      contents: "read"
      id-token: "write"

    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Google Cloud Auth
        id: auth
        uses: "google-github-actions/auth@v2"
        with:
          token_format: "access_token"
          workload_identity_provider: "${{ env.WIF_PROVIDER }}"
          service_account: "${{ env.SERVICE_ACCOUNT }}"

      - name: Docker Auth
        id: docker-auth
        uses: "docker/login-action@v3"
        with:
          username: "oauth2accesstoken"
          password: "${{ steps.auth.outputs.access_token }}"
          registry: "${{ env.GAR_LOCATION }}-docker.pkg.dev"

      - name: Set Service Name
        id: set-variables
        run: |-
          if [ "${{ github.ref }}" == "refs/heads/dev" ]; then
            echo "SERVICE=dev-go-app" >> $GITHUB_ENV
          else
            echo "SERVICE=go-app" >> $GITHUB_ENV
          fi          

      - name: Build and Push Image
        run: |-
          docker build -t "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO }}/go_${{ github.sha }}" -f server/Dockerfile.gcp ./server
          docker push "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO }}/go_${{ github.sha }}"          

      - name: Deploy to Cloud Run
        id: deploy
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: ${{ env.SERVICE }}
          region: ${{ env.REGION }}
          image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO }}/go_${{ github.sha }}

      - name: Show Output
        run: echo ${{ steps.deploy.outputs.url }}