Automate deployment with GitHub Actions

Development workflow - for automating deployment with Github Actions

This exercise walks through automating the deployment (Continuous deployment) of the front-end application using GitHub actions.

Objective

Create a system that automatically deploys the code to the S3 bucket and invalidates the caches on CloudFront whenever a pull request is merged or the code is pushed to the main branch on GitHub.

Description

In this exercise, we will be building a continuous deployment pipeline that will automatically deploy code whenever a Pull Request is merged to main branch in GitHub.

Our front-end static files are hosted on the S3 bucket (an object storage service offered by AWS), so any change or modification in the front-end code and static assets needs to be updated in the S3 bucket. New changes in the front end will not be reflected unless the files in the S3 bucket are updated.

GitHub is used for version control and the code in the main branch is deployed to the production version of the app. GitHub actions allow us to automate this entire deployment process when an event is triggered on the GitHub repo (Eg. when code is pushed to the main branch).

CloudFront is another AWS service that is used as a Content Delivery Network (CDN) and hence we need to invalidate the cache each time we make an update.

Without automated deployment, we would have to manually run commands that would trigger a deployment to the S3 bucket and invalidate the cache on the CloudFront.

Acceptance criteria

  • Create a workflow YAML (.yml) file in the repo.
  • Set up GitHub Action secrets in the repository
  • Add instructions in the YAML file that will install dependencies, build the code in the cloud container
  • Commit changes to the main repo
  • Monitor the Actions tab for successful deployment

Hints

  • Create a directory .github/workflows in the root of your project
  • Add your workflow file .yml in this .github/workflows directory
  • Set up GitHub Action secrets in the repository and add the following keys for the following properties:
    • AWS Access key AWS_ACCESS_KEY_ID
    • AWS Secret key AWS_SECRET_ACCESS_KEY
    • S3 Bucket name AWS_S3_BUCKET
    • CloudFront distribution id CLOUDFRONT_DISTRIBUTION_ID

Solution

Introduction: GitHub Actions

GitHub actions is a way to automate your workflows. You can set up GitHub actions to run whenever you push code to GitHub, or when someone opens a pull request. GitHub actions can also be triggered by events such as new issues, comments, or commits. GitHub actions is a great way to automate your development process and make sure that your code is always up-to-date.

GitHub Action allows you to create custom automated workflows directly into your GitHub repository. You can write individual tasks (actions) and combine them to create workflows. Workflow can be considered as automating all the tasks (actions) that you would otherwise perform manually.

In order to create a workflow, you would write down the steps in a configuration file.

Example: Install dependencies -> Build the project -> Deploy

GitHub Action uses YAML files to define this configuration.

You can configure to trigger a workflow when an event occurs on GitHub such as when a code is pushed to a branch, or when a pull request is created. Workflow can spin up one or more containers for you in the cloud, then you provide a set of steps or instructions for the container to do something useful. GitHub logs the progress of each step and makes it very clear if something fails.

 

When you visit your repo on GitHub you can see the “Actions” tab. This is where you can monitor your actions. When it is triggered it will give you a log of everything that has happened.

 

Development workflow:

development workflow

The above figure describes the development workflow.

  • The “Main” branch contains the code for the production version of the application.
  • For developing a new feature, a developer creates a “feature” branch from the stable “main” branch.
  • The developer adds commits and opens a Pull Request for the feature to be added to the main branch.
  • The code is reviewed and once approved, the developer merges the “feature” branch into the “main” branch.

 

Deployment workflow:

Development workflow - for automating deployment with Github Actions

The figure above describes the proposed deployment workflow.

  • GitHub actions workflow should be triggered once the feature branch is merged into the “main” branch or new code is pushed to the “main” branch.
  • GitHub actions should now install the dependencies, build the project, deploy to S3 bucket, and invalidate the cache on CloudFront.
  • If any of the steps fails, GitHub actions will show a red checkmark automatically and the deployment will be failed as expected. If everything goes according to the plan, the GitHub actions will show a green checkmark and the code will be deployed.

Create a workflow file:

In the source code create a new directory “.github/workflows” that GitHub Actions will watch to know which steps to execute. Anything in this workflow directory will be picked up by Github and set up as a workflow in the cloud.

Next, create a file “deploy.yml”. We will define the workflow in this YAML file.

mkdir .github/workflows/

touch .github/workflows/deploy.yml

Inside the “deploy.yml” file, we will begin by giving it the name “Production build”. name: Production Build

Then, we need to tell it on which event or events to run. We do that with the “on” object. In our example, we want to run it when code is pushed to the master branch.

on:
  push:
    branches:
    - master

Each workflow should have one or more jobs. You define these jobs on the “job” object. You can give the job the name “build” and from there we need to tell the job which VM to run on. We will run this on Ubuntu.

jobs:
  build:
    runs-on: ubuntu-latest

Next, we need to give this job instructions that will actually build and run this code.

First, we need to get the source code into the virtual machine using an action called “Checkout”. This brings the source code into the current working directory and that means you can run commands as you would from the command line

We also need to set up Node.js to run those commands. So we use the node setup action and then specify the version.

jobs:

  build:

    runs-on: ubuntu-latest

    strategy:

    matrix:

      node-version: [12.x]

    steps:

    - uses: actions/checkout@v1

    - name: Use Node.js ${{ matrix.node-version }}

      uses: actions/setup-node@v1

      with:

        node-version: ${{ matrix.node-version }}

Then, we are ready to start running our own commands. We need to install all dependencies using the npm install or npm ci command. Then, we will build the code with npm run build.

name property specifies a name for your step to display on GitHub.

action property selects an action to run as part of a step in your job. An action is a reusable unit of code.

Syntax to write the action: {owner}/{repo}@{ref}

In this action actions is the owner of the repo set-up node. And ref is the version v1.

- name: NPM Install

  run: |

    npm ci

- name: Production Build

  run: |

    npm run build

Next, we need to run commands that will deploy the built project to AWS. If we were doing this locally, we can use AWS CLI by authenticating with the AWS keys. Similarly, we can share a secret token with GitHub.

To deploy to our AWS S3 bucket from our GitHub Action we first need to configure two new secrets in our repository. These secrets are for our AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

Navigate to the Settings section of your GitHub repository and locate the Secrets section on the left-hand side.

Once there we are going to add a new secret for AWS_ACCESS_KEY_ID and paste in the access_key. Then we are going to add another secret for AWS_SECRET_ACCESS_KEY and paste in our secret_access_key.

GitHub will automatically encrypt this value for us and then we can access it from Action’s VM. This gives us a way to securely authenticate with AWS from the GitHub actions workflow.

Next, add the S3 bucket name (AWS_S3_BUCKET) and CloudFront invalidation id (CLOUDFRONT_DISTRIBUTION_ID) as secret keys respectively.

Now to deploy to AWS, we will use a third-party action from the Action Marketplace.

Select an “Actions” that takes care of all steps required to set up AWS CLI on GH Actions and run AWS commands.

For deploying to S3, we will use the S3-sync action: https://github.com/marketplace/actions/s3-sync

For CloudFront invalidation, we will use the invalidate CloudFront action:

https://github.com/marketplace/actions/invalidate-cloudfront

- name: Deploy to S3 production bucket

  uses: jakejarvis/s3-sync-action@master

  with:

    args: --acl public-read --delete

  env:

    AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}

    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

    AWS_REGION: 'us-east-1'

    SOURCE_DIR: "dist"

    DEST_DIR: "prod/"

The action will run the AWS command and then we tell it to use the arguments required to make our static assets public.

--acl public-read makes your files publicly readable

--delete permanently deletes files in the S3 bucket that are not present in the latest version of your repository/build.

It will be looking for environment variables of AWS tokens. We can access our secret GitHub values as follows: ${{ secrets.token }}

Similarly, we use actions to invalidate the CloudFront cache by setting the default parameters required by the action and AWS command (eg. distribution id, paths, and aws region) along with AWS access keys.

  uses: chetan/invalidate-cloudfront-action@master

  env:

    DISTRIBUTION: ${{ secrets.DISTRIBUTION_PRODUCTION_ID }}

    PATHS: '/*'

    AWS_REGION: 'us-east-1'

    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Now if we commit this new workflow file we should then be able to see in the Actions section of our GitHub repository that the job runs to completion.

We now have continuous deployment configured for our static website repository living on GitHub but deploying to our S3 bucket

Now, the above-created workflow will be triggered whenever a Pull Request is merged into the master branch.

It automatically builds and deploys the code to AWS, and the changes are reflected on the webpage.

  uses: chetan/invalidate-cloudfront-action@master

  env:

    DISTRIBUTION: ${{ secrets.DISTRIBUTION_PRODUCTION_ID }}

    PATHS: '/*'

    AWS_REGION: 'us-east-1'

    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Now if we commit this new workflow file we should then be able to see in the Actions section of our GitHub repository that the job runs to completion.

We now have continuous deployment configured for our static website repository living on GitHub but deploying to our S3 bucket

Now, the above-created workflow will be triggered whenever a Pull Request is merged into the master branch.

It automatically builds and deploys the code to AWS, and the changes are reflected on the webpage.

Managing workflow with GitHub Actions:

manage workflow- github actions

You can manage the workflow by navigating to the actions tab on your GitHub repo. The above image shows the successful completion of a GitHub Action that installed dependencies, built the project, deployed the code to the S3 bucket, and invalidated the cache.

YAML file for GitHub Actions

name: Automate deployment - Production build

on:
    push:
        branches: - main

jobs:
    build:
        runs-on: ubuntu-latest

        strategy:
            matrix:
                node-version: [12.x]

    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: NPM Install
    run: |
        npm ci

    - name: Production Build
    run: |
        npm run build

    - name: Deploy to S3 bucket
    uses: jakejarvis/s3-sync-action@master
    with:
        args: --acl public-read --delete
    env:
        AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION: 'us-east-1'
        SOURCE_DIR: "dist"
        DEST_DIR: "prod"

    - name: Invalidate CloudFront
    uses: chetan/invalidate-cloudfront-action@master
    env:
        DISTRIBUTION: ${{ secrets.DISTRIBUTION_STAGE_ID }}
        PATHS: '/\*'
        AWS_REGION: 'us-east-1'
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}