Brett Andrews

Cloud-Native Software Solutions

Continuous Deployment of Serverless applications to AWS using GitHub Actions

Brett Andrewsserverless

In my previous article I covered how to use Semantic Release and GitHub Actions to automate the build, test, and release workflow of a software package. It’s an awesome setup if you’re building a tool or library to be released to a package repository like NPM, but it falls short for deploying applications to the cloud.

In this post, we’ll build on that foundation to deploy the immutable, versioned artifacts to the cloud. I’ll be using AWS, the Serverless Framework and a Monorepo in this example, but the core GitHub Actions workflows can be applied to most projects.

Let’s review

/package.json
/packages/
  |ui/
    serverless.yaml
    package.json
  |db/
    serverless.yaml
    package.json
  |api/
    serverless.yaml
    package.json

Install semantic-release and its core plugins to the root package.json:

npm i -D semantic-release \
@semantic-release/commit-analyzer \
@semantic-release/release-notes-generator \
@semantic-release/changelog \
@semantic-release/npm \
@semantic-release/github \
@semantic-release/git

Create release.config.js:

module.exports = {
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    '@semantic-release/changelog',
    '@semantic-release/npm',
    ['@semantic-release/github', {
      assets: [
        'CHANGELOG.md',
        'release/db.zip',
        'release/api.zip',
        'release/ui.zip',
      ],
    }],
    '@semantic-release/git',
  ],
  preset: 'angular',
}

Add a package command to each service:

"scripts": {
  "package": "serverless package && zip -r api.zip .serverless serverless.yaml package.json package-lock.json"
}

Create .github/workflows/test-release.yaml:

name: Test and release

on:
  push:
    branches: [ master ]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        package: [db, api, ui]
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Setup Node.js
      uses: actions/setup-node@v1
      with:
        node-version: 12.x
    
    - name: Install root
      run: npm ci

    - name: Install package
      working-directory: "./packages/${{ matrix.package }}"
      run: npm ci

    - name: Test
      working-directory: "./packages/${{ matrix.package }}"
      run: npm test

    - name: Package
      working-directory: "./packages/${{ matrix.package }}"
      if: github.event_name == 'push' && github.ref == 'refs/heads/master'
      run: npm run package
    
    - name: Upload package artifact
      if: github.event_name == 'push' && github.ref == 'refs/heads/master'
      uses: actions/upload-artifact@v1
      with:
        name: release
        path: "./packages/${{ matrix.package }}/${{ matrix.package }}.zip"

  release:
    if: github.event_name == 'push' && github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    needs: [test]
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Setup Node.js
      uses: actions/setup-node@v1
      with:
        node-version: 12.x

    - name: Install root dependencies
      run: npm ci
      
    - name: Download package artifact
      uses: actions/download-artifact@v1
      with:
        name: release
        path: release
      
    - name: Release
      run: npx semantic-release
      env:
        # NOTE: GitHub Actions won't trigger on release when using the default GITHUB_TOKEN
        GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}

Unfortunately, you can’t use the default secrets.GITHUB_TOKEN secret as it doesn’t trigger a release event. 🤷‍♂️ Create a new GitHub Personal Access Token with repo scope and store the token as a new secret in the repository called GH_PERSONAL_ACCESS_TOKEN.

Create an ‘on release’ GitHub Action Workflow

Thankfully, one of the event triggers that GitHub Actions provides for us is on release. Let’s use that.

name: Deploy on release published

on:
  release:
    types:
    - published

jobs:
  deploy_prod:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        package: [db, api, ui]
    steps:
    - name: Download release
      uses: Legion2/download-release-action@v2.1.0
      with:
        repository: ${{ github.repository }}
        token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
        tag: latest
        file: ${{ matrix.package }}.zip

    - name: Unzip release package
      run: unzip ${{ matrix.package }}.zip

    - name: Setup AWS Credentials
      run: |
        mkdir ~/.aws
        echo "[civ6_prod]
        aws_access_key_id = ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws_secret_access_key = ${{ secrets.AWS_SECRET_ACCESS_KEY }}" > ~/.aws/credentials
    
    - name: Setup Node.js
      uses: actions/setup-node@v1
      with:
        node-version: 12.x

    - name: Install package
      run: npm i
    
    - name: Deploy package
      run: npx serverless deploy --package .serverless