May 09, 2021

Improving releases with service-splitting

Last week I published a piece on how running a CI-driven commit-based release infrastructure can improve your time-to-release drastically. I noticed that it wasn't super clear on how to get services running that aren't typically bundled or packaged up like container images, for example, frontend deployments. The issue here was that running all workflow steps in one routine every time something changed would take a long time, so usually, people put them into asynchronous flows, to run deployments to completion somewhere in the background, making it really hard to observe if something did not go to plan.

I've made a couple of adjustments to my mental model of deployments and iterated once more, coming to a much-improved flow: Splitting up all grouped service units into different flows.

In most cases, the frontend or mobile application deployments will integrate features after the backend has been shipped. Rolling back or making adjustments is also not one commit since multiple people might work on one feature in different branches, at different times.

To integrate all these demands, and get quick, reproducible build and deployment times coupled for the whole system, it makes sense to slightly decouple your backend services, all of which might be running on the same version (or on different commits depending on your infrastructure-as-code setup), and your frontend.

Let's imagine a flow where your backend will get packaged up into containers and deployed to staging for every merged pull request. Your frontend should also be deployed to your provider of choice on every merged pull request. Both of these flows should only be triggered if either the backend or frontend source or configuration files are touched.

backend-deploy-staging.yaml

The following file includes the GitHub actions workflow for deploying the example backend to staging after building and pushing container images for all backend-related service, whenever one of the said resources is modified (or when dispatching a manual job, which is super helpful).

name: Deploy backend to staging
on:
  workflow_dispatch:
  push:
    paths:
      # React to all backend-related changes (and workflow updates)
      - 'services/backend/**'
      - '.github/workflows/backend-deploy-staging.yaml'
    branches:
      - main
jobs:
  build-images:
    name: Build images
    runs-on: ubuntu-20.04
    steps:
      # Check out code
      - name: Checkout
        uses: actions/checkout@v2
      # Set up buildx
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      # Prepare Docker
      - name: Login to registry
        uses: docker/login-action@v1
      # Backend service
      - name: Build and push API
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: ...
          context: ./services/backend
  # Deploy to staging after running all image builds
  deploy-staging:
    name: Deploy staging
    runs-on: ubuntu-20.04
    needs:
      - build-images
    steps:
      # Check out latest code
      - name: Checkout
        uses: actions/checkout@v2
      # Start deployment via SSH
      - name:
        runs: |
          echo "Deployment started"

frontend-deploy-staging.yaml

name: Deploy frontend to staging
on:
  workflow_dispatch:
  push:
    paths:
      # React to all frontend-related changes (and workflow updates)
      - 'services/frontend/**'
      - '.github/workflows/frontend-deploy-staging.yaml'
    branches:
      - main
jobs:
  deploy-staging:
    name: Deploy staging
    runs-on: ubuntu-20.04
    steps:
      # Check out latest code
      - name: Checkout
        uses: actions/checkout@v2
      # Deploy frontend
      - name: Deploy staging
        working-directory: services/frontend
        run: |
          npm install
          npm run deploy

As you can see, those two files will make sure to kick off staging deployments for their respective services only if needed. When your frontend team now submits a change, it will be completely isolated from any backend action, making it much faster and more reliable. And vice-versa, if the backend team performs a change and subsequent release, the frontend stays in place.

This also prevents you from any unwanted incidents where services that should not have been deployed made it to production, forcing a rollback.

Releases work quite similarly, except we're not building images but using the pre-built ones. The frontend deployment is almost exactly the same, depending on your provider.