This exercise walks through automating the deployment (Continuous deployment) of the front-end application using GitHub actions.
Table of Contents
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
- AWS Access key
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:
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:
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:
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 }}