Breaking Changes: v2.0.0 to v3.0.0
Some time ago we wrote about Serverless deployments 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. No need for external CI/CD systems to generate new versions any more. GitHub also improved Releases, and Conventional Commits 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. 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 to calculate version numbers based on commit messages. The 'de facto' standard for these commit messages has been defined in Conventional Commits. 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 aPatch Release
or -
feat(users): create and update user details, closes #123
to create aMinor Release
You can find a quick summary here.
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, Building and testing Node.js Actions or even Custom JavaScript Actions.
We usually use a simple "test, build & release" Action, for example for our integration Actions like Cloudflare Preview URL.
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 for more information about security/permission steps taken in this Action.
./github/workflows/publish.yml:
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 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 (or AWS CodeBuild or Azure 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.
Cloud Build allows you to continously deploy your application. On GCP this works with a cloudbuild.yml
file:
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:
- fetch last image from cache (if there's a cached image)
- build the code and Dockerfile
- push the image to the registry
- deploy the image on Cloud Run
Note: We're using the new Artifact Registry instead of the deprecated Container Registry, so the registry urls are slightly different. You can create your Artifact Repository here and adjust the registry region in the cloudbuild file.
Go live on managed k8s
Google Cloud, AWS and Azure all have managed Kubernetes 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 (requires a credit card)
- enable the Cloud Build API
- enable the Cloud Run Admin API
- enable the Artifact Registry API
- create a Repository in the Artifact Registry (Format: Docker, Region: your choice)
If the build is green, you should see a new service in the Cloud 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. A Pull Request Preview is automatically deployed via the GitHub/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 or start a discussion on GitHub.
Top comments (0)