Build your own preview deployment service
August 25, 2020 / 15 min read
Last Updated: August 25, 2020Preview deployments are an essential step in the CI/CD pipelines of many frontend teams. The ability to preview every frontend change in a hosted and self-contained environment can increase the development velocity of a team quite significantly. Moreover, it brings more confidence that any newly added change will not bring any undesirable effect that would not get caught by automated tests before getting merged to production.
I wanted to bring this kind of service to my team at work, however, using one of the already available platforms that provided preview deployments out of the box such as Netlify, Vercel or Serverless was not an option. All our services and deployments were either managed on Google Cloud or Firebase. Thus, if we wanted a preview deployment service, we would have to build it on Google's Cloud platform.
Luckily, Google provides a great serverless service called Cloud Run. Cloud Run enables teams to deploy containers to production in a matter of seconds! Thus, I chose it as the service where the preview deployments would live, then built an automated pipeline around it that would deploy any change made to an app on every pull request and returned a URL to access that new version of that same app. In this article, we will go through each step to implement such an automated pipeline and build your own preview deployment service on Google Cloud Run as I did 🚀.
The perfect preview deployments developer experience
You might have seen this "preview deployments" feature in many other tools or SaaS out there, but I wanted to put together the list of elements that compose a great preview deployments developer experience before deep diving in my implementation. I used this list as my "north star" when building the automated pipeline and looking for how to host my previews, and the following are the key items I took into account:
- automated: whether it's on every push or every pull request event, the developer should not need to execute any command manually to make the preview operational.
- easily accessible: once deployed, your preview deployment should have a unique link that allows anyone to access that specific version of your frontend app.
- fast: the whole process of getting your app deployed should last no more than a couple of minutes
- self-actualized: each new change on the same branch or pull request should be deployed (preferably) on top of the other one
- running in a consistent environment: each preview should run in the same replicated environment
Considering these points, I knew I'd have to use Docker containers from the get-go to deploy my previews:
- their portability ensure that the environment of the preview deployments is constant.
- having one image per PR is easy: I could build the image and tag it with the number of the pull request. Each new change would be built and tagged with that same number, thus ensuring the image always contains the most up to date version of the UI for that PR.
Thus, the first steps of our preview deployments pipeline would consist of:
- Building our UI
- Building a Docker image
- Tagging our Docker image with the PR number
To help you get started here's one of the Dockerfile I always go back to build my frontend projects. It uses multistage builds and the image it outputs is very small:
Dockerfile sample to build and run an app in a containerized environment
1FROM node:12.18.3 as build2WORKDIR /usr/src/app3COPY package.json yarn.lock ./4RUN yarn5COPY . ./6RUN yarn build78FROM node:12.18.3-stretch-slim9COPY --from=build /usr/src/app/build /app10RUN yarn global add serve11WORKDIR /app12EXPOSE 300013CMD ["serve", "-p", "3000", "-s", "."]
Deploying and tagging services on Google Cloud Run
Considering the elements we listed in the previous part for the perfect preview deployments experience, it seemed that leveraging a serverless solution like Google Cloud Run is a great fit to deploy and run previews:
- it's cheap to run the different revisions of the app: you only pay for the traffic on the revisions
- each revision can have its own URL to be accessible: by tagging revisions, you can associate a tag to a revision which generates a unique URL for that revision
- it's fast: it only takes a few seconds to deploy services and revisions
- it's scalable: you can spin up to 1000 revisions per service! Once you reach that number, the oldest revisions will be simply removed from your service. Thus, no need to worry about taking down our revisions once we merge our pull request.
We will now look into each of the steps necessary to deploy a service, a revision of a service, and how to tag a revision on Google Cloud Run. The commands that will be listed in this section will eventually make it into a Github Workflow that we will detail in the next part.
Push the image to Google Cloud Registry (GCR)
First, we need to push the Docker image of our app that we built in the previous part:
1docker push gcr.io/PROJECTID/IMAGENAME:TAG
Replace PROJECTID with your project ID, IMAGENAME with the name of the image you built, and TAG with the tag of that image (the tag will matter the most in the next part that focuses on automating these steps)
Deploy a service on Cloud Run
Running the following command will allow us to deploy the Docker image we just pushed to GCR as a container on Cloud Run:
1gcloud beta run deploy "myapp" --image "gcr.io/PROJECTID/IMAGENAME:TAG" --platform managed --port=3000 --region=us-east1
myapp will be the name of your service on Cloud Run, you can replace it with whatever name you want
--port 3000
allows exposing the port 3000, you can replace it with whatever port your app uses
Our service is now deployed 🚀! We now have a URL for our service. Now let's look at the commands to deploy and tag a revision.
Deploy and tag a revision
Let's run the following command to deploy a revision for our services (remember to replace the name, project ID, image name, and tag with yours!)
1gcloud beta run deploy "myapp" --image "gcr.io/PROJECTID/IMAGENAME:TAG" --platform managed --revision-suffix=revision1 --port=3000 --region=us-east1
We now have a new revision for our service! This new revision uses the same Docker image and tag as our service. Eventually, we would want to deploy different versions of our app for each revision, which will result in each revision containing a change. We'll see in the next section how we can leverage Pull Request numbers and commit hashes to do that :smile.
One of the key element of the pipeline is tagging revisions: tagging a revision will let us have a unique URL for that revision.
If we have a service URL like https://myapp-abcdef123-ab.a.run.app, tagging it with "test" would give us the URL https://test---myapp-abcdef123-ab.a.run.app. To tag a revision we can run the following command:
1gcloud beta run beta update-traffic "myapp" --update-tags test=revision1 --platform=managed --region=us-east1
We now have all the key commands to deploy a service and a revision on Cloud Run and get back a unique URL for each revision! The next step is my personal favorite: automation.
Automating the deployments
In this part, we will create a Github workflow to execute the commands we just looked at on every Pull Request event.
The Github Workflow we're about to build is based on the Google Cloud Platform Github Actions, and more particularly we will implement a workflow that is similar to their Cloud Run Github Workflow example. I invite you to follow the README in this repository before continuing, it details how to:
- Create a service account
- Set up the service account's key and name as secrets of your project's Github repository.
I will use the same secret labels that they use in their workflow to make things easier for you to follow 😊.
In this part, we'll use a service account as the account that will run our commnands in the automated pipeline. This type of account is more suited than user accounts for that kind of tasks.
Considering that we now have a service account created and its key and name set as a secret of our Github repository, let's look at each step of the workflow on their own before looking at the entire pipeline:
- First, we have to set up our workflow to **run on every pull requests **against our
main
branch:
1name: Preview Deployment23on:4pull_request:5branches:6- 'main'
- Run the checkout action and setup node action:
1---2steps:3- name: Checkout Commit4uses: actions/checkout@v25with:6ref: ${{ github.event.pull_request.head.sha }}7- name: Use Node.js ${{ matrix.node-version }}8uses: actions/setup-node@v19with:10node-version: ${{ matrix.node-version }}
- Then we need to install and configure the GCloud SDK and beta components using our service account name and secret key:
1---2- name: Setup Google Cloud SDK3uses: GoogleCloudPlatform/github-actions/setup-gcloud@master4with:5project_id: ${{ secrets.PROJECTID }}6service_account_key: ${{ secrets.RUN_SA_KEY }}7export_default_credentials: true8- name: Install Google Cloud SDK Beta Components9run: gcloud components install beta
- Let's not forget to configure Docker, as we showed earlier, to be able to push on GCR
1---2- name: Setup Docker for GCR3run: gcloud auth configure-docker
- Build and Push our Docker image using the PR number as a tag:
1---2- name: Build Docker Image3run: docker build -t gcr.io/${secrets.PROJECTID}/IMAGENAME:${{github.event.number}}4- name: Push Docker Image To GCR5run: docker push gcr.io/${secrets.PROJECTID}/IMAGENAME:${{github.event.number}}
- Get the commit hash of the HEAD commit of this PR. This is necessary because every revision suffix must be unique, and commit hashes are very handy to generate unique strings 😊:
1---2- name: Get HEAD Commit Hash3id: commit4run: echo "::set-output name=hash::$(git rev-parse --short HEAD)"
- **Deploy a new revision on Cloud Run. **
1---2- name: Deploy Revision On Cloud Run3run: gcloud beta run deploy "myapp" --image "gcr.io/${secrets.PROJECTID}/IMAGENAME:${{github.event.number}}" --no-traffic --platform managed --revision-suffix=${{github.event.number}}-${{steps.commit.outputs.hash}} --port=3000 --region=us-east1
- Tag the revision:
1---2- name: Tag Revision On Cloud Run3run: gcloud beta run services update-traffic "myapp" --update-tags pr-${{github.event.number}}=myapp-${{github.event.number}}-${{steps.commit.outputs.hash}} --platform=managed --region=us-east1
- Post the comment on the PR containing the URL! This will let your reviewers know how to access the revision that's just been deployed. I used the add-pr-comment Github Action. You could use any other action or even build your own (!), as long as you can pass your revision URL as an argument:
1---2- name: Post PR comment with preview deployment URL3uses: mshick/add-pr-comment@v14env:5GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}6with:7message: |8Successfully deployed preview revision at https://pr-${{github.event.number}}---myapp-abcdef123-ab.a.run.app9allow-repeats: false
Here's how the full workflow file looks like:
Preview Deployment Github Workflow
1name: Preview Deployment23on:4pull_request:5branches:6- 'main'78jobs:9deploy-to-cloud-run:10runs-on: ubuntu-20.0411strategy:12matrix:13node-version: [12.x]14steps:15- name: Checkout Commit16uses: actions/checkout@v217with:18ref: ${{ github.event.pull_request.head.sha }}19- name: Use Node.js ${{ matrix.node-version }}20uses: actions/setup-node@v121with:22node-version: ${{ matrix.node-version }}23- name: Setup Google Cloud SDK24uses: GoogleCloudPlatform/github-actions/setup-gcloud@master25with:26project_id: ${{ secrets.PROJECTID }}27service_account_key: ${{ secrets.RUN_SA_KEY }}28export_default_credentials: true29- name: Install Google Cloud SDK Beta Components30run: gcloud components install beta31- name: Setup Docker for GCR32run: gcloud auth configure-docker33- name: Build Docker Image34run: docker build -t gcr.io/${secrets.PROJECTID}/IMAGENAME:${{github.event.number}}35- name: Push Docker Image To GCR36run: docker push gcr.io/${secrets.PROJECTID}/IMAGENAME:${{github.event.number}}37- name: Get HEAD Commit Hash38id: commit39run: echo "::set-output name=hash::$(git rev-parse --short HEAD)"40- name: Deploy Revision On Cloud Run41run: gcloud beta run deploy "myapp" --image "gcr.io/${secrets.PROJECTID}/IMAGENAME:${{github.event.number}}" --no-traffic --platform managed --revision-suffix=${{github.event.number}}-${{steps.commit.outputs.hash}} --port=3000 --region=us-east142- name: Tag Revision On Cloud Run43run: gcloud beta run services update-traffic "myapp" --update-tags pr-${{github.event.number}}=myapp-${{github.event.number}}-${{steps.commit.outputs.hash}} --platform=managed --region=us-east144- name: Post PR comment with preview deployment URL45uses: mshick/add-pr-comment@v146env:47GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}48with:49message: |50Successfully deployed preview revision at https://pr-${{github.event.number}}---myapp-abcdef123-ab.a.run.app51allow-repeats: false
We now have a fully functional preview deployment workflow! Now let's circle back to the first part and go through the checklist to see whether or not this automated preview deployment pipeline covers all the criteria we established:
- automated: thanks to the workflow we just detailed above, our preview deployment service will automatically deploy our app on every PR against the main branch
- easily accessible: the last step of our workflow covers that as it will post the URL of a given deployment as a PR comment.
- fast: it only takes a few minutes to build and ship our app! Additionally, we leveraged multi-stage build to make the Docker Image of our app lighter which speeds up a bit the workflow when it comes to pushing the image to GCR.
- self-actualized: for every new commit, the workflow will be executed, plus, thanks to the way we tag our revisions, the URL will remain constant through changes for a given PR!
- running in a consistent environment: each revision is built following the same recipe: the Dockerfile we introduced in the second part!
I had a lot of fun building this (I'm a big fan of automation!) and I hope you liked this post and that it will inspire you to build more automation for your team to make it ship amazing things even faster 🚀! If you and your team are also in the process of establishing other elements of a CI/CD pipeline on top of the one we just saw, I'd recommend checking out The little guide to CI/CD for frontend developers that summarizes everything I know about CI/CD that can make team unstoppable!
Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
Have a wonderful day.
– Maxime
Do you want to increase your team's development velocity and collaboration? Preview Deployments are one of the key pieces for that, and this post details how to implement your own serverless deployment service based on Google Cloud Run.