CI/CD Pipeline with Github Actions to deploy to GCP 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:
- (Essentials and GCP Setup)
- Checkout Repo
- Authenticate in Google Cloud
- Docker Login (Artifact Registry)
- Build and Push Image
- 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:
- Artifact Registry Writer (To upload images to Artifact Registry)
- Cloud Run Developer (To deploy service with the new image)
- Service Account User (To let this account be impersonated by WIF)
- 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 accountGAR_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 }}