At Seed, we’ve built a fully managed CI/CD pipeline for Serverless Framework apps on AWS. So you can imagine we have quite a bit of experience with the various CI/CD services.

Over the next few weeks we are going to dive into some of the most popular services out there. We’ll take a detailed look at what it takes to run your own CI/CD pipeline for Serverless apps. This’ll give you a good feel for not just how things work but how Seed makes your life easier!

Today we’ll be looking at CircleCI. You might have come across tutorials that help you set up a CI/CD pipeline for Serverless on Circle. However, most of these are way too simplistic and don’t talk about how to deploy large real-world Serverless apps.

Instead we’ll be working with a more accurate real-world setup comprising of:

A monorepo Serverless app

With multiple services

Deployed to separate development and production AWS accounts

As a refresher, a monorepo Serverless app is one where multiple Serverless services are in subdirectories with their own serverless.yml file. Here is the repo of the app that we’ll be configuring you can refer to. The directory structure might look something like this:

/ package.json services/ users-api/ package.json serverless.yml posts-api/ package.json serverless.yml cron-job/ package.json serverless.yml

What we’ll be covering

How to deploy your monorepo Serverless app on Git push How to deploy to multiple AWS accounts How to deploy using the pull request workflow How to clean up unused branches and closed pull requests

Note that, this guide is structured to work in standalone steps. So if you only need to get to be able to deploy to multiple AWS accounts, you can stop after step 2.

Also, worth mentioning that while this guide is helping you create a fully-functional CI/CD pipeline for Serverless; all of these features are available in Seed without any configuration or scripting.

Pre-requisites

A CircleCI account.

AWS credentials (Access Key Id and Secret Access Key) of the AWS account you are going to deploy to. Follow this guide to create one.

A monorepo Serverless app in a GitHub repo. Head over to our template repo and click on Use this template to clone it to your account.

1. How to deploy your monorepo app on Git push

Let’s start by configuring the Circle side of things.

Go into your CircleCI account. Select Contexts from the left menu, and click Create Context.

Create a context called Development.

Go in to the Development context, and click on Add Environment Variable.

Create an variable with:

Name: AWS_ACCESS_KEY_ID

Value: Access Key Id of the IAM user

Repeat the previous step and create another variable with:

Name: AWS_SECRET_ACCESS_KEY

Value: Secret Access Key of the IAM user

Go to the cloned repository and click on Create new file.

Name the new file .circleci/config.yml and paste the following:

version : 2.1 jobs : deploy-service : docker : - image : circleci/node:8.10 parameters : service_path : type : string stage_name : type : string steps : - checkout - restore_cache : keys : - dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} - dependencies-cache - run : name : Install Serverless CLI command : sudo npm i -g serverless - run : name : Install dependencies command : | npm install cd << parameters.service_path >> npm install - run : name : Deploy application command : | cd << parameters.service_path >> serverless deploy -s << parameters.stage_name >> - save_cache : paths : - node_modules - << parameters.service_path >>/node_modules key : dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} workflows : build-deploy : jobs : - deploy-service : name : Deploy Users API service_path : services/users-api stage_name : ${CIRCLE_BRANCH} context : Development - deploy-service : name : Deploy Posts API service_path : services/posts-api stage_name : ${CIRCLE_BRANCH} context : Development - deploy-service : name : Deploy Cron Job service_path : services/cron-job stage_name : ${CIRCLE_BRANCH} context : Development

Let’s quickly go over what we are doing here:

We created a job called deploy-service , that takes the path of a service and the name of the stage you want to deploy to. The name of the stage will be used as the --stage in the Serverless commands.

, that takes the and the you want to deploy to. The name of the stage will be used as the in the Serverless commands. The deploy-service job does an npm install in the repo’s root directory and in the service subdirectory.

job does an in the repo’s root directory and in the service subdirectory. The job then goes into the service directory and runs serverless deploy with the stage name that’s passed in.

with the stage name that’s passed in. We also created a workflow that runs the deploy-service job for each service, while passing in the branch name as the stage name.

job for each service, while passing in the as the stage name. As a side note, we also specified that we want to cache the node_modules/ directory in both the root and the service directory for faster deployment.

Next, scroll to the bottom and click Commit new file.

Back in Circle, select ADD PROJECTS from the left menu, and click on the Set Up Project button next to your project. Make sure the Show forks checkbox is checked.

Select Linux as the Operating System, and Node as the Language. Go to step 5 and click on Start building.

Then click on WORKFLOWS in the left menu.

Click on the workflow, you will see the 3 jobs that are currently running.

Click on a job. You will see the output for each of the steps. Scroll down to the Deploy application section, and you should see the output for the serverless deploy -s master command.

Now that we have the basics up and running, let’s look at how to deploy our app to multiple AWS accounts.

2. How to deploy to multiple AWS accounts

You might be curious as to why we would want to deploy to multiple AWS accounts. It’s a good practice to keep your development and production environments in separate accounts. By separating them completely, you can secure access to your production environment. This will reduce the likelihood of accidentally removing resources from it while developing.

To deploy to another account, repeat the earlier step of creating a Development context, and create a Production context with the AWS Access Key Id and Secret Access Key of your production AWS account.

Go to your GitHub repo and open .circleci/config.yml . Replace it with the following:

version : 2.1 jobs : deploy-service : docker : - image : circleci/node:8.10 parameters : service_path : type : string stage_name : type : string steps : - checkout - restore_cache : keys : - dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} - dependencies-cache - run : name : Install Serverless CLI command : sudo npm i -g serverless - run : name : Install dependencies command : | npm install cd << parameters.service_path >> npm install - run : name : Deploy application command : | cd << parameters.service_path >> serverless deploy -s << parameters.stage_name >> - save_cache : paths : - node_modules - << parameters.service_path >>/node_modules key : dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} workflows : build-deploy : jobs : # non-master branches deploys to stage named by the branch - deploy-service : name : Deploy Users API service_path : services/users-api stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master - deploy-service : name : Deploy Posts API service_path : services/posts-api stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master - deploy-service : name : Deploy Cron Job service_path : services/cron-job stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master # master branch deploys to the 'prod' stage - deploy-service : name : Deploy Users API service_path : services/users-api stage_name : prod context : Production filters : branches : only : master - deploy-service : name : Deploy Posts API service_path : services/posts-api stage_name : prod context : Production filters : branches : only : master - deploy-service : name : Deploy Cron Job service_path : services/cron-job stage_name : prod context : Production filters : branches : only : master

This does a couple of things:

A Git push to the master branch will be deployed to the prod stage, instead of the stage with the branch name. It’ll also use the Production context.

branch will be deployed to the stage, instead of the stage with the branch name. It’ll also use the context. A Git push to all the other branches will be deployed to the stage with their branch name using the Development context.

Commit and push this change. This will trigger Circle to build the master branch again. This time deploying to your production account.

Next, let’s look at implementing the PR aspect of our Git workflow.

3. How to deploy in pull request workflow

A big advantage of using Serverless is how easy and cost effective it is to deploy many different versions (ie. stages) of your app. A great use case for this is to deploy a version of your app for each pull request to preview how the merged version would work, similar to the idea of Review Apps on Heroku.

Unfortunately, Circle does not support pull requests natively. It can be achieved with a little bit of bash scripting. Let’s look at how to set that up.

Go to your GitHub repo and open the .circleci/config.yml that we had created above. Replace it with the following:

version : 2.1 jobs : deploy-service : docker : - image : circleci/node:8.10 parameters : service_path : type : string stage_name : type : string steps : - checkout - run : name : Check Pull Request command : | if [[ ! -z "$CIRCLE_PULL_REQUEST" ]]; then # parse pr# from URL https://github.com/fwang/sls-monorepo-with-circleci/pull/1 PR_NUMBER=${CIRCLE_PULL_REQUEST##*/} echo "export PR_NUMBER=$PR_NUMBER" >> $BASH_ENV echo "Pull request #$PR_NUMBER" fi - run : name : Merge Pull Request command : | if [[ ! -z "$PR_NUMBER" ]]; then git fetch origin +refs/pull/$PR_NUMBER/merge git checkout -qf FETCH_HEAD fi - restore_cache : keys : - dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} - dependencies-cache - run : name : Install Serverless CLI command : sudo npm i -g serverless - run : name : Install dependencies command : | npm install cd << parameters.service_path >> npm install - run : name : Deploy application command : | cd << parameters.service_path >> if [[ ! -z "$PR_NUMBER" ]]; then serverless deploy -s pr$PR_NUMBER else serverless deploy -s << parameters.stage_name >> fi - save_cache : paths : - node_modules - << parameters.service_path >>/node_modules key : dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} workflows : build-deploy : jobs : # non-master branches deploy to stage named by the branch - deploy-service : name : Deploy Users API service_path : services/users-api stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master - deploy-service : name : Deploy Posts API service_path : services/posts-api stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master - deploy-service : name : Deploy Cron Job service_path : services/cron-job stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master # master branch deploy to the 'prod' stage - deploy-service : name : Deploy Users API service_path : services/users-api stage_name : prod context : Production filters : branches : only : master - deploy-service : name : Deploy Posts API service_path : services/posts-api stage_name : prod context : Production filters : branches : only : master - deploy-service : name : Deploy Cron Job service_path : services/cron-job stage_name : prod context : Production filters : branches : only : master

Let’s go over the changes we’ve made.

We added a Check Pull Request step. We check the built-in environment variable $CIRCLE_PULL_REQUEST to decide if the current branch belongs to a pull request. If it does, then we set an environment variable called $PR_NUMBER with the pull request id.

step. We check the built-in environment variable to decide if the current branch belongs to a pull request. If it does, then we set an environment variable called with the pull request id. We also added a Merge Pull Request step, where we checkout the merged version of code from GitHub.

step, where we checkout the merged version of code from GitHub. We then change the Deploy application step to deploy to the name the stage pr# when deploying a pull request.

Commit these changes to .circleci/config.yml .

Next, go to GitHub and create a pull request to the master branch. This will trigger Circle to deployed the merged code with stage name pr1 to your development account.

4. How to clean up unused branches and closed pull requests

After a feature branch is deleted, or a pull request closed, you want to clean up the resources in your AWS account. Circle does not have triggers for these events on GitHub. If you have the credentials for your AWS account, you can perhaps remove the resources by going into each service directory on your local machine and running serverless remove . This step is both cumbersome and not ideal.

To get around this limitation, we’ll use a little trick. We’ll tell Circle to remove a stage (instead of deploying to one) if it sees a Git tag called rm-stage-STAGE_NAME .

To do this, go to your GitHub repo and open .circleci/config.yml that we created above. Replace it with the following:

version : 2.1 jobs : deploy-service : docker : - image : circleci/node:8.10 parameters : service_path : type : string stage_name : type : string steps : - checkout - run : name : Check Pull Request command : | if [[ ! -z "$CIRCLE_PULL_REQUEST" ]]; then # parse pr# from URL https://github.com/fwang/sls-monorepo-with-circleci/pull/1 PR_NUMBER=${CIRCLE_PULL_REQUEST##*/} echo "export PR_NUMBER=$PR_NUMBER" >> $BASH_ENV echo "Pull request #$PR_NUMBER" fi - run : name : Merge Pull Request command : | if [[ ! -z "$PR_NUMBER" ]]; then git fetch origin +refs/pull/$PR_NUMBER/merge git checkout -qf FETCH_HEAD fi - restore_cache : keys : - dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} - dependencies-cache - run : name : Install Serverless CLI command : sudo npm i -g serverless - run : name : Install dependencies command : | npm install cd << parameters.service_path >> npm install - run : name : Deploy application command : | cd << parameters.service_path >> if [[ ! -z "$PR_NUMBER" ]]; then serverless deploy -s pr$PR_NUMBER else serverless deploy -s << parameters.stage_name >> fi - save_cache : paths : - node_modules - << parameters.service_path >>/node_modules key : dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} remove-service : docker : - image : circleci/node:8.10 parameters : service_path : type : string stage_name : type : string steps : - checkout - restore_cache : keys : - dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} - dependencies-cache - run : name : Install Serverless CLI command : sudo npm i -g serverless - run : name : Install dependencies command : | npm install cd << parameters.service_path >> npm install - run : name : Remove application command : | cd << parameters.service_path >> # parse stage name from TAG rm-stage-pr1 serverless remove -s << parameters.stage_name >> - save_cache : paths : - node_modules - << parameters.service_path >>/node_modules key : dependencies-cache-{{ checksum "package-lock.json" }}-{{ checksum "<< parameters.service_path >>/package-lock.json" }} workflows : build-deploy : jobs : # non-master branches deploy to stage named by the branch - deploy-service : name : Deploy Users API service_path : services/users-api stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master - deploy-service : name : Deploy Posts API service_path : services/posts-api stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master - deploy-service : name : Deploy Cron Job service_path : services/cron-job stage_name : ${CIRCLE_BRANCH} context : Development filters : branches : ignore : master # master branch deploy to the 'prod' stage - deploy-service : name : Deploy Users API service_path : services/users-api stage_name : prod context : Production filters : branches : only : master - deploy-service : name : Deploy Posts API service_path : services/posts-api stage_name : prod context : Production filters : branches : only : master - deploy-service : name : Deploy Cron Job service_path : services/cron-job stage_name : prod context : Production filters : branches : only : master # remove non-production stages - remove-service : name : Remove Users API service_path : services/users-api stage_name : ${CIRCLE_TAG:9} context : Development filters : tags : only : /^rm-stage-.*/ branches : ignore : /.*/ - remove-service : name : Remove Posts API service_path : services/posts-api stage_name : ${CIRCLE_TAG:9} context : Development filters : tags : only : /^rm-stage-.*/ branches : ignore : /.*/ - remove-service : name : Remove Cron Job service_path : services/cron-job stage_name : ${CIRCLE_TAG:9} context : Development filters : tags : only : /^rm-stage-.*/ branches : ignore : /.*/

Let’s look at what we changed here:

We created a new job called remove-service . It’s similar to our deploy-service job, except it runs serverless remove . It assumes the name of the tag is of the format rm-stage-STAGE_NAME . It parses for the stage name after the 2nd hyphen.

. It’s similar to our job, except it runs . It assumes the name of the tag is of the format . It parses for the stage name after the 2nd hyphen. We also set the workflow to run the remove-service job for each of our services.

To test this, try tagging the pull request branch before you close the PR.

$ git tag rm-stage-pr1 $ git push --tags

You’ll notice that the pr1 stage will be removed from your development account.

And that’s it! Let’s wrap things up next.

Next steps

It took us a few steps but we now have a fully-functional CI/CD pipeline for our monorepo Serverless app. It supports a PR based workflow and even cleans up once a PR is merged. The repo used in this guide is available here with the complete CircleCI configs.

Some helpful next steps would be to auto-create custom domains for your API endpoints, send out Slack or email notifications, generate CloudFormation change sets and add a manual confirmation step when pushing to production, etc. You also might want to design your workflow to accommodate for any dependencies your services might have. These are cases where the output from one service is used in another.

Finally, if you are not familiar with Seed, it’s worth noting that it’ll do all of the above for you out of the box! And you don’t need to write up a build spec or do any scripting.