Deploying to AWS Elastic Beanstalk with GitHub Actions

February 28, 2022

views
DevOps

Objective

You've got an application deployment going on AWS Elastic Beanstalk. Maybe you have even completed my previous tutorial on deploying a Next.js app in Docker on AWS Elastic Beanstalk.

Whatever the case, deploying your app manually sucks and you are feeling the pain. You want to automate the repetitive deployment process away. In this article, we will go over the process of setting up a basic CI/CD pipeline for your app using GitHub Actions.

What is CI/CD?

CI/CD refers to two related but different DevOps concepts related to managing software delivery Continuous Integration (CI) and Continuous Delivery (CD).

Continuous Integration (CI)

Understanding continuous integration requires going back in time. Back during prehistoric times, software projects were slow-moving dinosaurs. Different teams would work on different features in isolation. The changes the different teams would make would then be integrated into the project in batches. This process had quite a high probability of resulting in some form of integration hell wherein changes would be incompatible with one another and different parts of the project would break.

Nowadays, changes tend to be integrated much more frequently. A common workflow is to have different features being worked on on different branches and merged back as soon as possible into the main branch of the project.

Continuous Integration refers to the process by which changes are validated and integrated back into the main branch. Such steps may including compiling source code or building a Docker image, and running tests against the new build.

Continuous Delivery (CD)

Continuous delivery is the second stage of the CI/CD pipeline. After CI has completed successfully - i.e. build is ready and passed tests - an automated workflow runs to automatically deploy the changes.

What deploying changes means differs depending on the project. Deploying could refer to pushing a Docker image to a container registry, or publishing a package to NPM or updating a live app on a server or cluster. It is also common to deploy to different environments before deploying to production.

Continuous delivery automates a process that back in the prehistoric era required intensive manual labor such as SSH-ing into servers. A variant of continuous integration is continuous deployment, which emphasizes the immediacy of deployments. In practice, the acronym CD is often used to refer to both continuous delivery and continuous deployment.

GitHub Actions

GitHub Actions is a relatively recent GitHub feature that lets you easily run a CI/CD workflow on GitHub to build, test and deploy your changes. You can define workflows that run whenever a Pull Request is opened or merged or whenever

With GitHub Actions, you define workflows in a YAML file that is to be stored in .

A workflow is made up of steps, actions and jobs that are run inside a runner:

  • step: a shell command or an action
  • action: a custom application for GitHub Actions, we won't need this here
  • job: a series of steps that are run sequentially on the same runner
  • runner: the environment within which a job is run, you can think of it as a container

Here's an example of a workflow:

The workflow is named and contains one single job named that consists of 4 steps:

  1. is an action that will check out the repository in the runner - without this, your workflow cannot access the code in your repository.
  2. is an action that will set up Node.js on the runner.
  3. is a step that will install NPM dependencies. Note that is only available because Node.js was installed in the previous step.
  4. is a step that will run an NPM command to run tests.

Additionally, defines an event that means the workflow will be triggered everytime somebody pushes something to the repository.

A note about GitHub-hosted runners

If you want to find out more about how GitHub runners work, see here.

The basic idea you need to understand to use GitHub Actions correctly is that a runner is a virtual machine, and each step is like a shell command that you would run if you were to install a fresh VM.

For example, if you were to set up a VM, you could only use after you have installed Node.js. The same applies in a workflow where running a step requires the runner to have its dependencies installed in a previous step.

Deploying an app on AWS Elastic Beanstalk

If you have followed my previous tutorial on deploying a Dockerized application on AWS Elastic Beanstalk.

AWS Elastic Beanstalk lets you deploy an app to an AWS Elastic Beanstalk runtime and manages an EC2 cluster and related infrastructure for you. The workflow for deploying an app on an existing Elastic Beanstalk environment is always the same regardless of the runtime to deploy the app into, be it Node.js, .NET or Docker.

Running will zip up your application, upload it into an S3 bucket, which will then be pulled by your Elastic Beanstalk environment. If your runtime is Docker, it will expect to find a at the root of your project directory and will build the image inside one of your cluster's EC2 instances before running it as a container. If your runtime is anything else (e.g. Node.js, etc.), it will run the code directly.

So let's think about it at a high level to try and figure out what steps we need to complete to achieve what we want. We ultimately want to run , so we can walk backwards from there and recursively craft the steps we need.

requires having the EB CLI installed and configured. This implies we need to install the EB CLI and configure it. We can find out how to do that by looking at the AWS documentation on installing the EB CLI and the AWS documentation on configuring the EB CLI.

There are multiple ways of configuring the EB CLI (command-line arguments, config files, environment variables), we will be using environment variables here.

Environment variables?

If you are a noob when it comes to DevOps type things, you might be wondering why we would choose to configure the AWS CLI using environment variables and not one of the many other methods of configuring it.

Environment variables are variables that are accessible to the entire system. They are often used to set parameters such as API keys, credentials and other such variables that should not be hardcoded for security reasons - nobody wants their API keys to end up in the git commit history.

You can set environment variables in a container image or any OS with just one command. Platforms such as GitHub let you define environment variables as GitHub secrets that you can access from inside a GitHub Actions runner.

So let's cook up a rough draft of what we would need to run in a shell if we were trying to do all that manually:

We got the basic steps down, now we need to convert that into a GitHub Actions workflow.

Let's pick a runner. A common runner is , which as its name suggests is the latest version of Ubuntu.

Let's define the steps now - we need to install Python, install the EB CLI, set some environment variables and then run . Looking around on GitHub, we find that there is an existing action that can install Python for us, so we will use it.

Let's not forget to also set the environment variables that will be used by the EB CLI to make the API calls it needs to AWS:

That's it, we've got everything we need in our workflow. We can put all these blocks together into a job that we will name and that we want to trigger every time somebody pushes to the branch:

Configuring GitHub Secrets

We use environment variables in our workflow, but we have not set their values anywhere. Let's do it now using GitHub secrets.

GitHub secrets are a feature available through the settings of your repository on GitHub. A secret is a key-value pair that is encrypted and that you can access from within a GitHub Actions runner.

  1. On your repository, go to Settings.

  2. In the Settings tab, select Secrets > Actions in the left sidebar.

  3. Click New repository secret in the upper right hand corner.

  4. Input your secret and click Add secret:

  1. Repeat the process for and :

That's it, your secrets are now accessible inside your GitHub Actions workflow with this syntax:

Monitoring GitHub Actions pipeline

You can then view your pipeline status on your GitHub repository.

The Actions tab on your repository shows all the runs of your CI/CD pipeline and you can view the details of the run, including all the shell output in the UI.