MonoRepo CI/CD using CodeBuild and Git Branching Workflow

Michael Andrews
Monsoon Engineering
4 min readSep 16, 2019

By Michael Andrews

At Monsoon, we are building an AWS Cloud Native application using AWS AppSync, Lambda, RDS and Kinesis. One of the challenges with any Cloud Native application is continuously integrating and deploying (CI/CD) the application in repeatable and reliable fashion to several deployment environments (development, staging, production). In order accomplish this, we leverage two other AWS technologies, CodeBuild and CloudFormation, and integrate them with some basic UNIX environment variables and our Git branching workflow.

CI/CD with AWS CodeBuild and Git Branches

Each project in our Cloud Native application is built and deployed using CodeBuild. The first step when creating a new component starts with provisioning a CodeBuild instance in the deployment environment using CloudFormation. Each of our high level deployment environments (development, staging, production) are essentially a separate AWS Account (AWS_PROFILE) and a file containing a collection of environment variables (AWS_ENV). CodeBuild stores the collection of environment variables so that we can run multiple environments concurrently. Additionally, the CodeBuild instances are connected to our source code repository, GitHub, which triggers a build when the source is modified.

Since the majority of our codebase is constructed using AWS Lambda, we only leverage two CodeBuild phases — prebuild to compile, lint and unit test the code, and postbuild to deploy the packaged Lambda function using the AWS CLI. One limitation of using CodeBuild in this fashion, is that each GitHub PushEvent can trigger a build and deploy to any environment. We use two environment variables to control this process GIT_BUILD_BRANCH_FILTER and GIT_DEPLOY_BRANCH_FILTER. The GIT_BUILD_BRANCH_FILTER indicates that a build should happen in this CodeBuild instance when the Git branch matches the filter. The GIT_DEPLOY_BRANCH_FILTER indicates that a deployment should happen if the build passes in this CodeBuild instance. Using a few combinations of these basic environment variables, it’s simple and straightforward to model a standard Git branching workflow (development, staging, production):

# development environment
$ cat AWS_ENV=development.sh
AWS_PROFILE=development
AWS_NAMESPACE=development
GIT_BUILD_BRANCH_FILTER='^*$'
GIT_DEPLOY_BRANCH_FILTER='^development$'
# staging environment
$ cat AWS_ENV=staging.sh
AWS_PROFILE=staging
AWS_NAMESPACE=staging
GIT_BUILD_BRANCH_FILTER='^staging$'
GIT_DEPLOY_BRANCH_FILTER='^staging$'
# production environment
$ cat AWS_ENV=production.sh
AWS_PROFILE=production
AWS_NAMESPACE=production
GIT_BUILD_BRANCH_FILTER='^master$'
GIT_DEPLOY_BRANCH_FILTER='^master$'

NOTE: We always want to run a build against the development branch so that GitHub can report the build status and prevent us from committing code that doesn’t pass the unit tests:

This effectively ties the git branch to the environment in a simple yet effective solution using only environment variables.

Effective CI/CD with a MonoRepo

If you’re planning to build a Cloud Native application using AWS Lambda, you will need to effectively manage micro-services, as each function in the application will require it’s own Lambda. When integrating this pattern with your source code version control system, you could theoretically place each Lambda in its own source code repository, but this can quickly become cumbersome. A more scalable approach to this N+1 repository problem is to simply start with a single monolithic repository, or MonoRepo, and add each component as a sub-project or sub-folder.

Once you commit to having your entire application live within a single monolithic repository, you now face a couple of new challenges:

  1. How does the CI/CD build and test only the sub-projects that have changed?
  2. How does the CI/CD deploy only the sub-projects that have changed

In order solve these challenges effectively, we can leverage Git again and check if the sub-project has changed relative to the GIT_DEPLOY_BRANCH_FILTER. If the sub-project hasn’t changed then it’s safe to skip both the build and deploy entirely! NOTE: There is one limitation with GitHub specifically–it only supports a maximum of 20 WebHooks per repository. This limitation can be circumvented using a WebHook proxy which we plan to detail in a separate article.

Personal Cloud Native Development Environment

As discussed in our previous article, Running AWS SAM in a Docker Container, each service can be run locally using Docker and AWS SAM. However, when it comes to Cloud Native testing and integration with AWS services (Kinesis, SQS, RDS and Lambda), ideally each developer can quickly provision their own personal Cloud Native Development Environment. Using the approach outlined above, this is as simple as provisioning a new environment and pushing up a Git branch!

$ cat mandrews.env.sh
AWS_PROFILE=development
AWS_NAMESPACE=development
GIT_BUILD_BRANCH_FILTER='^mandrews-*$'
GIT_DEPLOY_BRANCH_FILTER='^mandrews-*$'
$ AWS_ENV=mandrews.env.sh bin/provision.sh
$ git checkout development
$ git checkout -b mandrews-branch
$ git push origin mandrews-branch

Conclusion

As we have demonstrated, it’s possible to model an effective Git development workflow using AWS CodeBuild, a few simple shell scripts and some environment variables. Using this approach provides the added benefit of allowing each developer to have their own personal Cloud Native development environment! We hope you found this article to be illuminating. All of the sample code referenced in the article can be found in this GitHub repository. Next up, we plan to detail an approach to automated end-to-end testing for Cloud Native applications using Cypress and GitOps!

Special Thanks to Chris Ramón for collaborating on the design and implementation of our Git development workflow.

--

--