Serverless v3.0.0
Tags: serverless, googlecloud, continuousdeployment, semanticversioning
Published: 2021-11-23 00:00:00

---
date: '2021-11-23T00:00:00.000Z'
created_at: '2021-11-23T00:00:00.000Z'
edited_at: '2021-11-23T00:00:00.000Z'
crossposted_at: '2021-12-15T09:55:32.161Z'
---

## Breaking Changes: v2.0.0 to v3.0.0

Some time ago we wrote about [Serverless deployments v2.0.0](https://zentered.co/articles/serverless-v2-0-0/) and a lot has changed and improved since then, so it's time for an update.

One of the biggest improvements to the workflow is [GitHub Actions](https://github.com/features/actions). No need for external CI/CD systems to generate new versions any more. GitHub also [improved Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases), and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) reached v1.0.0.

## Continous Deployment of Serverless Applications

Let's walk through the details of a fully automated continous integration and deployment system with all those fancy new toys. The one thing that hasn't changed since the last article is our Git workflow: we recommend the [Git Feature Branch Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow). The merge strategy doesn't matter much, rebase, squash and merge all work with the following tools and flows.

### Use Conventional Commits

We use [Semantic Release](https://github.com/semantic-release/semantic-release) to calculate version numbers based on commit messages. The 'de facto' standard for these commit messages has been defined in [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). In short, your commit messages start with a keyword, a scope and then a short description of the change. Commit messages should be written in present tense ("closes", not "closed"):

- `fix(accounts): calculation error on totals, closes #123` to create a `Patch Release` or
- `feat(users): create and update user details, closes #123` to create a `Minor Release`

You can find a [quick summary here](https://www.conventionalcommits.org/en/v1.0.0/#summary).

### Add Semantic Release and GitHub Actions

Actions is a CI/CD product from GitHub. You can define "workflows" as simple `yml` files to go through certain steps such as linting, testing, building etc. They work for pretty much any project / programming language. Every Action ususally has a trigger ("on") and jobs. You can read more about [Events that trigger workflows](https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows), [Building and testing Node.js Actions](https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs-or-python) or even [Custom JavaScript Actions](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action).

We usually use a simple "test, build & release" Action, for example for our integration Actions like [Cloudflare Preview URL](https://github.com/zentered/cloudflare-preview-url/blob/main/.github/workflows/publish.yml).

Below you can find a slightly more complex Action that restricts permissions and runs checkout, npm install, linter, codestyle checks, tests, coverage reports and semantic release. See [Hardening GitHub Actions](https://zentered.co/articles/hardening-github-actions-in-private-repositories/) for more information about security/permission steps taken in this Action.

**./github/workflows/publish.yml**:

```yaml
name: Semantic Release

on:
  push:
    branches:
      - 'main'

permissions:
  actions: none
  checks: none
  contents: write
  deployments: write
  issues: read
  packages: none
  pull-requests: write
  repository-projects: none
  security-events: none
  statuses: none

jobs:
  publish:
    env:
      CI: true
      NODE_ENV: test

    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2.4.1
        with:
          node-version: '16'
          cache: 'npm'
      - name: Install Dependencies
        run: npm ci --ignore-scripts
      - name: Run Linter
        run: npm run lint
      - name: Validate Codestyle
        run: npm run codestyle
      - name: Test
        run: npm test
        env:
          DISABLE_REQUEST_LOGGING: true
          LOG_LEVEL: error
      - name: report coverage
        uses: codecov/codecov-action@v2.1.0
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
      - name: Semantic Release
        run: npx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

The magic line to convert our Conventional Commit messages into Releases is `run: npx semantic-release`. This will run semantic-release after the tests, lint etc. are done and generate a release with fancy notes based on the commits that have been merged. For example, if you currently have a `v1.2.0` deployed, and merge a `fix`, semantic-release will publish `v1.2.1`.

You can [see how Semantic Releases look like](https://github.com/zentered/cloudflare-preview-url/releases) in our open source repos. The important piece here is, that each release also comes with a `tag`. This is interesting and useful for a variety of reasons, for example compliance ("what has been deployed when?"), and of course the developer experience (DX). You can see the currently deployed version in each environment, without checking cryptic 6-digit SHAs. The tag is also the ingredient required for the next step:

### Automated Deployment

In [Google Cloud Build](https://cloud.google.com/build) (or [AWS CodeBuild](https://docs.aws.amazon.com/codebuild/latest/userguide/create-project-console.html) or [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/)) you can select a source repository for deployments and pick "tags" as a build trigger. `.v*` for example will deploy every new release. If you want to deploy a _Major_ release in a new environment, you can switch the previous trigger settings to `.v1.*` to only deploy `v1` releases, but not `v2`. This is a nice way to test breaking changes.

![screenshot of cloud build trigger settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6rzkh80299ng1iqmu7b.png)
 

Cloud Build allows you to continously deploy your application. On GCP this works with a `cloudbuild.yml` file:

```yml
steps:
  - name: 'gcr.io/cloud-builders/docker'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        docker pull us-central1-docker.pkg.dev/$PROJECT_ID/api-demo/api:latest || exit 0
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - build
      - -t
      - us-central1-docker.pkg.dev/$PROJECT_ID/api-demo/api:$TAG_NAME
      - -t
      - us-central1-docker.pkg.dev/$PROJECT_ID/api-demo/api:latest
      - .
      - --cache-from
      - us-central1-docker.pkg.dev/$PROJECT_ID/api-demo/api:latest
  - name: 'gcr.io/cloud-builders/docker'
    args:
      ['push', 'us-central1-docker.pkg.dev/$PROJECT_ID/api-demo/api:$TAG_NAME']
  - name: 'gcr.io/cloud-builders/gcloud'
    args:
      - run
      - deploy
      - api-demo
      - --image=us-central1-docker.pkg.dev/$PROJECT_ID/api-demo/api:$TAG_NAME
      - --region=us-central1
      - --memory=256Mi
      - --platform=managed
      - --allow-unauthenticated
      - --min-instances=0
      - --max-instances=5
images:
  - 'us-central1-docker.pkg.dev/$PROJECT_ID/api-demo/api:$TAG_NAME'
  - 'us-central1-docker.pkg.dev/$PROJECT_ID/api-demo/api:latest'
timeout: 1800s
```

`$TAG_NAME` is the version number, so all your images and deployments have a semantic version number and you can clearly identify what's running where. There are four steps here:

1. fetch last image from cache (if there's a cached image)
2. build the code and Dockerfile
3. push the image to the registry
4. deploy the image on Cloud Run

_Note:_ We're using the new [Artifact Registry](https://cloud.google.com/artifact-registry/) instead of the deprecated Container Registry, so the registry urls are slightly different. You can create [your Artifact Repository here](https://console.cloud.google.com/artifacts/create-repo) and adjust the registry region in the cloudbuild file.

### Go live on managed k8s

Google Cloud, AWS and Azure all have managed [Kubernetes](https://kubernetes.io) products which are great to focus on software development and leave the k8s configs, helm charts and autoscale yamls to the _DevOps Pros_. In the previous section we defined a step to deploy to Cloud Run. For GCP, you may need to enable the API, billing etc. on your project:

- enable [Billing](https://console.cloud.google.com/billing) (requires a credit card)
- enable the [Cloud Build API](https://console.cloud.google.com/apis/api/cloudbuild.googleapis.com/overview)
- enable the [Cloud Run Admin API](https://console.cloud.google.com/apis/api/run.googleapis.com/overview)
- enable the [Artifact Registry API](https://console.cloud.google.com/apis/api/artifactregistry.googleapis.com/overview)
- [create a Repository in the Artifact Registry](https://console.cloud.google.com/artifacts/create-repo) (Format: Docker, Region: your choice)

If the build is green, you should see a new service in the [Cloud Run](https://cloud.google.com/run/) console. When you click on the service, at the top you see the new public URL (ie. `app-name-123.run.app`) for your service. Link a domain to that URL and 🎉, your API is now live.

## Summary

Let's recap a everyday workflow:

- Developer picks an issue and works on this amazing new feature for your product
- Developer pushes changes to a feature branch and opens a [Draft Pull Request](https://github.blog/2019-02-14-introducing-draft-pull-requests/). A Pull Request Preview is automatically deployed via the [GitHub/Google Cloud Build](https://github.com/apps/google-cloud-build) integration and changes can be tested
- Developer marks the Pull Request as "Ready for Review"
- Team reviews the PR, discusses the changes and approves (hopefully ;))
- Pull Request is merged into `main` by the developer or reviewer
- GitHub `publish.yml` Action is triggered ("push to main") and runs tests, lint etc.
- If everything goes well, `npx semantic-release` executes, calculating a new version number, summarizing the release notes and creates a new tag and release
- Google Cloud Build trigger is fired on "new tag", builds your app and pushes the Docker image to the Artifact Registry
- Cloud Build starts the Cloud Run deployment with the new image (which has the version number)
- Cloud Run starts the new service and changes the traffic allocation to the new instance
- The change is live, start over with the next feature 😎

Cloud Run enables you to split traffic between multiple revisions, so you can perform gradual rollouts such as canary deployments or blue/green deployments. Combined with automatic rollbacks, you can skip your staging environment alltogether and continously deploy to production. Developers can easily revert changes by merging a commit to `main`.

Thanks for reading! If you have any questions or comments, please [reach out on Twitter](https://twitter.com/zenteredco) or [start a discussion on GitHub](https://github.com/zentered/zentered/discussions/new?category=ideas-feedback).