GitHub (Preview) Deployments with Google Cloud Platform
Patrick
Posted on May 30, 2023
Motivation and Introduction
Build previews aren't anything new or innovative. Heroku had them for years. If you're building on Google Cloud Platform however, things are a little more difficult. Google Cloud Build can be used to deploy to Cloud Functions, Cloud Run, Kubernetes, App Engine etc. etc. This is what makes previews slightly complicated, as Cloud Build doesn't know where you're deploying your application or service to. They have a GitHub App to connect the repositories and show the build status in GitHub, but that's it. No previews, comments or any of the "normal" stuff we're used to from products like Heroku, Vercel or Fly.
There's an "easy" way to post a comment on a GitHub Pull Request from Cloud Build with a simple HTTP request. But why easy when you can use the GitHub Deployments API.
With this API, you can create Environments, have different variables per environment and show build & deployment status directly on a Pull Request. And once the deployment is done on your CD platform, you can add the URL to the deployment status. This is what we're going to do in this article.
Again, there's another way of doing this by using the Deployments API directly with HTTP requests. This can be tricky sometimes though and you'll need to define a Personal Access Token to authenticate with GitHub. Instead, we decided to put together a little Node.js service that uses GitHub Apps to generate an app token and interact with the Deployments API. This allows a little finer control over the deployment process.
Disclaimer
This article is exclusively written for Google Cloud Platform and GitHub. Basic understanding of Google Cloud Platform, Google Cloud Build and IAM permissions is assumed. You should have a Google Cloud Platform project set up.
GitHub Deployer (Node.js & Docker)
GitHub Deployer is a tiny Node.js service that authorized with a GitHub App and interacts with the GitHub Deployments API. Commands are predefined and some information needs to be built in to the Docker image, as otherwise the runtime configuration in Cloud Build would get too complex.
The Docker image is not ready built; you'll need to build it yourself and push it to your registry. The best place is Google Artifact Registry where the images for your application are stored as well (Container Registry is deprecated):
- If not done already, create an Artifact Registry repository
- Create a GitHub App and install it on your organization
- Create a GitHub App
- Required permissions: 'pull_requests', 'deployments', 'metadata'
- Copy the App ID (from the App Settings screen), and the App Installation ID (after clicking "Configure" you'll find the installation ID in the URL. No idea where else that can be found)
- Create & download a private key and convert it to base64 (
base64 -i your.private-key.pem
)
Run the Docker image locally
docker run -it -e REPO_NAME=test -e REF=main -e ENVIRONMENT=test us-central1-docker.pkg.dev/[PROJECT]/docker/gh-deployer:latest create
Required environment variables:
-
REPO_NAME
: required the name of the repository (e.g.gh-deployer
) -
REF
: required the branch, tag or SHA to deploy (e.g.main
) -
ENVIRONMENT
: required the environment to deploy to (e.g.preview
) -
TRANSIENT_ENVIRONMENT
: optional/false if set totrue
, the deployment will be deleted after a certain time -
DESCRIPTION
: optional a description of the deployment
The following commands are available as first argument:
-
create
- create a new deployment -
pending
- the build is pending -
in_progress
- the build is in progress -
queued
- the build is queued -
success
- the deployment was successful -
error
- something went wrong -
failure
- the deployment failed
With Cloud Build the options are currently a bit limited. There's no pending
state as Cloud Build needs to start in order to create the initial deployment. queued
doesn't make much sense either, so in our own setup, we're using the following:
-
gh-deployer/create
to create the transient deployment for a commit -
pgh-deployer/ending
directly after create - Run kaniko build to build the Docker image for deployment
-
gh-deployer/in_progress
after the build is done and before the image is deployed -
gh-deployer/success
after the image is deployed
Again, with Cloud Build we don't know where the deployment is going to be, so there are two ways to pass the deployment URL to the success
step:
- In a build step, write the URL into
/workspace/deployer_environment_url
- Pass a second argument to the Docker image after
success
We use Cloud Run for our deployments, so here's the build step to retrieve the deployment URL for a given Pull Request number:
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
env:
- PR_NUMBER=$_PR_NUMBER
script: >-
gcloud run services list --project [project] --filter preview-$PR_NUMBER
--format "value(status.address.url)" > /workspace/deployer_environment_url
Cloud Build Configuration / Terraform
cloudbuild.yaml
Here's a complete exmaple using the cloudbuild.yaml
configuration file. We use Kaniko to build and Cloud Run as deplyoment target.
If you work with Terraform, there's a Terraform file afailable as well: preview.tf
steps:
- name: 'us-central1-docker.pkg.dev/[project]/docker/gh-deployer:latest'
env:
- REPO_NAME=$REPO_NAME
- TRANSIENT_ENVIRONMENT=true
- DESCRIPTION=Deploying to Google Cloud Run
- ENVIRONMENT=preview
- REF=$REF_NAME
args:
- create
- name: 'us-central1-docker.pkg.dev/[project]/docker/gh-deployer:latest'
env:
- REPO_NAME=$REPO_NAME
- ENVIRONMENT=preview
- REF=$REF_NAME
args:
- pending
- name: 'gcr.io/kaniko-project/executor:latest'
args:
- '--destination=us-central1-docker.pkg.dev/[project]/docker/[name]:$SHORT_SHA'
- '--use-new-run'
- name: 'us-central1-docker.pkg.dev/[project]/docker/gh-deployer:latest'
env:
- REPO_NAME=$REPO_NAME
- ENVIRONMENT=preview
- REF=$REF_NAME
args:
- in_progress
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
args:
- run
- deploy
- preview-$_PR_NUMBER
- '--image=us-central1-docker.pkg.dev/[project]/docker/[name]:$SHORT_SHA'
- '--region=us-central1'
- '--memory=512Mi'
- '--platform=managed'
- '--allow-unauthenticated'
entrypoint: gcloud
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
env:
- PR_NUMBER=$_PR_NUMBER
script: >-
gcloud run services list --project [project] --filter preview-$PR_NUMBER --format "value(status.address.url)" > /workspace/deployer_environment_url
- name: 'us-central1-docker.pkg.dev/[project]/docker/gh-deployer:latest'
env:
- REPO_NAME=$REPO_NAME
- ENVIRONMENT=preview
- REF=$REF_NAME
args:
- success
GitHub Actions
By using the Deployments API, you can trigger GitHub Actions on deployment_status
. This makes it easy to run quality assurance checks, end-to-end tests etc. on each preview build. Here's an example for running Playwright on each preview using the environment_url
that is passed in for the success
state:
name: preview-qa
on: deployment_status
jobs:
playwright-qa:
if: ${{ github.event.deployment_status.state == 'success' }}
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Install Playwright Browsers
run: npm exec playwright install --with-deps -y
- name: Run Playwright tests
run: pnpm exec playwright test
env:
BASE_URL: ${{github.event.deployment_status.environment_url}}
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Optimizations
- the basic image
gcr.io/google.com/cloudsdktool/cloud-sdk
is relatively large. You can build your own small gcloud image with Cloud Run components to speed up the builds.
Conclusion
Comments on GitHub Pull Requests are easy at first, but when you have multiple deployments per Pull Request, failing builds etc. they don't offer a lot of flexibility and are hard to maintain / update. The GitHub Deployments API offers an elegant way to create and manage deployments from any third party CI/CD system. With the GitHub Deployer
we tried to streamline the interaction with the deployments API and take care of some side effects or pitfalls that are impossible to solve with just the HTTP API.
Another benefit of using the GitHub Deployments API is that you can use the deployment_status
trigger for GitHub Actions and get the environment_url
directly with the event payload.
You can find a little more detailled installation / build instructions and all the code on the GitHub repository
If you have any questions or comments, please reach out on BlueSky / Twitter or join the discussion on GitHub.
Posted on May 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.