TL;DR
- ☁️ We'll learn how to deploy on Google Cloud Run
- ✨ We'll learn how to design and implement a modern workflow with GitHub Actions
- 🤿 We'll see code snippets of real-world workflows
If you want to learn how to deploy like Vercel or Netlify with Google Cloud, this is the right place for you.
Vercel and Netlify both offer seamless transition from development to shipping your features and they make
Let's go.
The Modern Workflow Design
If you are familiar with Vercel or Netlify, you'll notice the
To design the workflow, we can break it down to two paths:
- Production workflow: triggered by push events in the main branch.
- Preview workflow: triggered by push and pull_request events in all branches except main.
Let's look into the prod workflow first.
Production Workflow
The production workflow does only one thing: live deployment. When a pull request merges into main, it triggers the workflow to deploy your production build to
The workflow looks like this:
Set up Google Cloud
- Authenticate to Google Cloud
- Setup Google
Cloud SDK - Authorize to push docker containers to
Artifact Registry
Push Docker Image to Artifact Registry
- Generate image tag
- Build docker container
- push docker container to registry
Deploy
- Deploy to Cloud Run
Set up Google Cloud
If you don't have a project on Google Cloud yet, follow
this guide to create one. I'll name my project awesome-project.
To authenticate the workflow to access Google Cloud, we can use the
env:
PROJECT_ID: 'awesome-project'
SERVICE: 'homepage'
REGION: 'us-west1' # ☘️Low CO2
REGISTRY: '[YOUR_REGISTRY_ID]'
IMAGE_NAME: 'live'
WORKLOAD_IDENTITY_PROVIDER: '[YOUR_WORKLOAD_PROVIDER_ID]'
SERVICE_ACCOUNT: '[YOUR_SERVICE_ACCOUNT_ID]'
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/[email protected]'
id: 'auth'
with:
token_format: 'access_token'
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.SERVICE_ACCOUNT }}
Here we are authenticating via
a service account a workload identity provider to grand IAM roles to the Workload Identity Provider .
Congratulations, you've done it! It's the most difficult part of the workflow.
Now we can set up Cloud SDK and authorize the workflow to be able to push Docker containers to Artifact Registry:
steps:
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v0
- name: Authorize Docker push
run: gcloud auth configure-docker ${{ env.REGISTRY }}
You can follow
Push Docker Image to Artifact Registry
We are now able to push containers so let's dockerize your project and tag your image:
steps:
- name: Generate Image Tag
id: image-tag
run: |
image_tag="$REGISTRY/$PROJECT_ID/$SERVICE/$IMAGE_NAME:${GITHUB_SHA::8}"
echo "tag=$image_tag" >> $GITHUB_OUTPUT
- name: Build Docker Container
run: |
docker build -t ${{ steps.image-tag.outputs.tag }}
- name: Push Docker Container
run: |
docker push ${{ steps.image-tag.outputs.tag }}
Deploy
Now you are ready to deploy to cloud run using the docker container:
steps:
- name: Deploy to Cloud Run
run: |
gcloud run deploy ${{ env.SERVICE }} \
--platform "managed"
--region ${{ env.REGION }} \
--image ${{ steps.image-tag.outputs.tag }}
Cloud Run will assign 100% of the traffic to this deployment by default so all visitors will be directed to this revision.
In the Cloud Console, you'll find a URL to your Cloud Run deployment. It looks like this:
The Complete GitHub Actions Production Workflow:
prod-ci.yaml
name: Production Workflow
on:
push:
branches:
- main
env:
PROJECT_ID: 'awesome-project'
SERVICE: 'homepage'
REGION: 'us-west1' # ☘️Low CO2
REGISTRY: '[YOUR_REGISTRY_ID]'
IMAGE_NAME: 'live'
WORKLOAD_IDENTITY_PROVIDER: '[YOUR_WORKLOAD_PROVIDER_ID]'
SERVICE_ACCOUNT: '[YOUR_SERVICE_ACCOUNT_ID]'
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/[email protected]'
id: 'auth'
with:
token_format: 'access_token'
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.SERVICE_ACCOUNT }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v0
- name: Authorize Docker push
run: gcloud auth configure-docker ${{ env.REGISTRY }}
- name: Generate Image Tag
id: image-tag
run: |
image_tag="$REGISTRY/$PROJECT_ID/$SERVICE/$IMAGE_NAME:${GITHUB_SHA::8}"
echo "tag=$image_tag" >> $GITHUB_OUTPUT
- name: Build Docker Container
run: |
docker build -t ${{ steps.image-tag.outputs.tag }}
- name: Push Docker Container
run: |
docker push ${{ steps.image-tag.outputs.tag }}
- name: Deploy to Cloud Run
run: |
gcloud run deploy ${{ env.SERVICE }} \
--platform "managed"
--region ${{ env.REGION }} \
--image ${{ steps.image-tag.outputs.tag }}
Preview Workflow
The preview workflow is similar to production with a few modifications:
Set up Google Cloud
- Authenticate to Google Cloud
- Setup Google Cloud SDK
- Authorize to push docker containers to Artifact Registry
Push Docker Image to Artifact Registry
- Get Task Id from Reference
- Generate image tag
- Build docker container
- push docker container to registry
Deploy
- Deploy revision with tag
- Comment preview URL in the pull request
Let's take a look at the differences.
Push Docker Image to Artifact Registry
The first difference is tagging the Docker image. In the production workflow, we use a constant $IMAGE_NAME
in environment variables to tag the production image. However, for previews, we want to use an identifier that represents the pull request. We are using the first 8 characters of the branch name as the identifier:
steps:
- name: Get Task Id from Reference
id: task
run: |
name="${{ github.ref_name }}"
lowercase="${name,,}"
echo "id=${lowercase:0:8}" >> $GITHUB_OUTPUT
As an example I'll name the branch TASK-123-awesome-workflow. The task step will extract the task ID task-123 from the branch name.
If you are curious about the shell script syntax, check out
Shell Parameter Expansion .
The identifier is set to be in lowercase because we'll use it for tagging the Cloud Run revision later. The naming convention of
- it allows lowercase characters
- it allows numbers
- it allows "-"
- it has a maximum length limit of 63 characters
Next we can use the task id to tag the image, build a container, and push to the registry:
steps:
- name: Generate Image Tag
id: image-tag
run: |
image_tag="$REGISTRY/$PROJECT_ID/$SERVICE/${{ steps.task.outputs.id }}:${GITHUB_SHA::8}"
echo "tag=$image_tag" >> $GITHUB_OUTPUT
- name: Build Docker Container
run: |
docker build -t ${{ steps.image-tag.outputs.tag }}
- name: Push Docker Container
run: |
docker push ${{ steps.image-tag.outputs.tag }}
Deploy
Now we are ready to deploy the preview. The deployment is similar to live deployment with two differences:
- Unlike live deployment, we want to assign 0% of the traffic to the preview revision.
- We want to give the deployment a different URL other than the live URL.
We can use --tag and --no-traffic parameters to achieve them:
steps:
- name: Deploy Revision with Tag
run: |
gcloud run deploy ${{ env.SERVICE }} \
--platform "managed" \
--region ${{ env.REGION }} \
--image ${{ steps.image-tag.outputs.tag }} \
--tag pr-${{ steps.task.outputs.id }} \
--no-traffic
After running the step successfully, you'll get the preview URL like this:
Finally, we can post a comment about the preview URL on the pull request:
steps:
- name: Comment Preview URL in PR
uses: mshick/add-pr-comment@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
message: |
🍿 Successfully deployed preview revision at https://pr-${{ steps.jira.outputs.id }}---homepage-12345abcde-ez.a.run.app
allow-repeats: false
The Complete GitHub Actions Preview Workflow
preview.yaml
name: Preview Workflow
on:
push:
branches-ignore:
- main
pull_request:
branches-ignore:
- main
workflow_run:
workflows: ['Dev CI']
types: [completed]
env:
PROJECT_ID: 'awesome-project'
SERVICE: 'homepage'
REGION: 'us-west1'
REGISTRY: '[YOUR_REGISTRY_ID]'
WORKLOAD_IDENTITY_PROVIDER: '[YOUR_WORKLOAD_PROVIDER_ID]'
SERVICE_ACCOUNT: '[YOUR_SERVICE_ACCOUNT_ID]'
jobs:
preview:
runs-on: ubuntu-20.04
permissions:
pull-requests: 'write'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/[email protected]'
id: 'auth'
with:
token_format: 'access_token'
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.SERVICE_ACCOUNT }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v0
- name: Authorize Docker push
run: gcloud auth configure-docker ${{ env.REGISTRY }}
# Get task id in lowercase from branch name for docker image naming convention
# More detail on base parameter expansion: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
- name: Get Task Id from Reference
id: task
run: |
name="${{ github.ref_name }}"
lowercase="${name,,}"
echo "id=${lowercase:0:8}" >> $GITHUB_OUTPUT
- name: Generate Image Tag
id: image-tag
run: |
image_tag="$REGISTRY/$PROJECT_ID/$SERVICE/${{ steps.task.outputs.id }}:${GITHUB_SHA::8}"
echo "tag=$image_tag" >> $GITHUB_OUTPUT
- name: Build Docker Container
run: |
docker build -t ${{ steps.image-tag.outputs.tag }}
- name: Push Docker Container
run: |
docker push ${{ steps.image-tag.outputs.tag }}
- name: Deploy Revision with Tag
run: |
gcloud run deploy ${{ env.SERVICE }} \
--platform "managed" \
--region ${{ env.REGION }} \
--image ${{ steps.image-tag.outputs.tag }} \
--tag pr-${{ steps.jira.outputs.id }} \
--no-traffic
- name: Comment Preview URL in PR
uses: mshick/add-pr-comment@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
message: |
🍿 Successfully deployed preview revision at https://pr-${{ steps.jira.outputs.id }}---homepage-12345abcde-ez.a.run.app
allow-repeats: false
Final Thoughts
We've just re-created the modern workflow from Vercel and Netlify! There are a few improvements to make the workflow more robust:
- use a short hash as the identifier to tag preview image and Cloud Run revision
- replace the hard coded live URL with a step output in the preview comment step
- add a workflow that cleans up the unused Cloud Run revisions and Docker containers
References
GitHub: auth GitHub Action Article: Enabling keyless authentication from GitHub Actions - Google Cloud Website: Vercel Previews Website: Netlify Deploy Previews Website: Vercel Website: Netlify Document: Shell Parameter Expansion - GNU Google Cloud Article: Ex-Principal Engineer's Guide to Design Thinking and Continuous Delivery - Daw-Chih Liou Document: Cloud Run - Google Cloud Website: GitHub Actions Document: DevOps tech: Trunk-based development - Google Cloud Website: Artifact Registry - Google Cloud Website: Cloud SDK - Google Cloud Document: Workload identity federation - Google Cloud Document: Create and manage service account keys- Google Cloud Document: Configure workforce identity federation - Google Cloud Document: Store Docker container images in Artifact Registry - Google Cloud Document: Creating and managing projects - Google Cloud Document: Use tags for testing, traffic migration and rollbacks - Google Cloud GitHub: add-pr-comment Github: setup-gcloud GitHub Action
This article was originally posted on Daw-Chih’s website.