Automation with CircleCI Running Assets Pipeline, Tests , Code Quality And Deployment to S3

Being a fast paced growing startup it’s necessary to have a powerful automated process to ease development for the company’s developers (#developer-experience). There are plenty of manual processes that you might find yourself writing scripts for that can be automated and speed up development.

Whats and Why CI, Benefits and Costs

The Baseline idea behind CI (continuous integration) is the agility of writing code in fast pace, delivered to production by developers on a daily basis, first proposed by Grady Booch in 1991.

The benefits and costs of such working environment

  1. Bugs can be fixed immediately based on urgency.
  2. Cross teams & cross feature code alignment helps preventing integration conflicts and bugs.
  3. The main benefit is writing code in incremental chunks. Switching from working-few-weeks-than-deploy to deploy-every-2–3 days, and maintaining fast paced development (Hence, more PR’s, more tickets, etc.).
  4. Early collection of metric data (e.g, how many times HTTP requests will be made for a non-existing feature).
  5. Features need to be “hidden” (with feature flag or hardcoded toggle, preferably feature flag, so you could turn off buggy feature without deployment/revert).
  6. Calculated risks safe environment, which allow for fast development since we have “safety nets” during development.

As can be seen in the above diagram, problems can be encapsulated to short period of development process (see red rectangle number 2). This is an essential part of a productive and high quality development process.. If you’ll find yourself having an architectural mistake or a bug during the early stages of development you’ll end up caring that problem for a long time.

As we can see, if we develop new feature or product for a long period of time without our workflow that includes running tests, PR/code review, code quality checks, QA by another developer etc. We’ll find it really hard to get rid of mistakes that we’ve carried for weeks.

Use Cases for Automation

Here some ideas for automated process that we can use automation like CircleCI for:

Code Quality

  1. Unit tests.
  2. Integration tests.
  3. Performance benchmarks.
  4. Codeclimate/codecov.
  5. Code linting (csslint, eslint).
  6. Security detectors.
  7. Commit msg guidelines (e.g start commit with [FEAT/BUG/FIX]).

Deployment

  1. Per env deployment (production, development, production like, staging etc).
  2. Static assets deployment.
  3. Static documentation sites.
  4. Package publication (npm publish).

Commutation

  1. Organization slack webhook (to know when someone push to master).
  2. Task manager / Jira integration (branch name should be the ticket in Jira so the new feature can update the status of a jira ticket automatically).
  3. Automated PR/git issue for consumer of a package.
  4. Automated documentation generator.

Scripts

  1. Update internal servers (via curl or having external API).
  2. Deleting files.
  3. Monitoring of application (e.g how big the bundle size of client asset).

The benefits of having a good automated process in your work flow is an amazing time & life saver, With automated process you and your team can write lots of code and cut development time by up to 10%. In our company the automated processes allows us to take greater “risks” (Due to the ability to re-deploy bugfix quickly and response to production existing bug) and write a lot of code, since we build robust “safety nets”. Without those safety nets, devs will forget to run tests, to clean their code or it would take them a long time to deploy or revert.

Basic Circle Configuration

The first example is the very basic automated CI that use node version 11 and simply install npm dependencies, and cache the result of node_modules folder (for later usage).

# Check https://circleci.com/docs/2.0/language-javascript/ for more details
version: 2.1

jobs:
  # Job #1
  build:
    # The primary container is an instance of the first image listed. The job's commands run in this container.
    docker:
      # specify the version you desire here
      - image: circleci/node:11.10
    working_directory: ~/app
    steps:
      - checkout
      # cache dependencies (to speed automation process)
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "package.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-
      - run:
          name: Install NPM dependencies - via yarn
          command: yarn install
      - run:
          name: Build Production Assets
          command: yarn run build
      - persist_to_workspace:
          # Must be an absolute path, or relative path from working_directory. This is a directory on the container which is 
          # taken to be the root directory of the workspace.
          root: ~/app
          # Must be relative path from root
          paths:
            - node_modules  # contain dependencies 
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}

Caching our node_modules folder is very important for the next builds, that’s what makes the difference between 15 minutes a build to 5 minutes build. And with the help of “persist to workspace we’ll be able to reuse folders that we’ve created at an early stage of the process within the next job, so in this example we won’t have to install npm dependencies again and again for each job.

Basic workflows configuration

Workflows in CircleCI provide us the opportunity to organize, Filter branches/jobs and provide environment context for each job.

// … few jobs

workflows:
  version: 2.1
  build-test-and-deploy:
    jobs:
      - build: # job #1 - here we build and prepare everything we need 
          context: dev
      - test: # job #2 - is where we run our tests, linting, code coverage etc.
          context: dev	# for each job we can define “context” with environment variables (and define those in CircleCI dashboard as admin)
          requires:
            - build	# Before we can continue, we need the “build” job to finish, our tests depend on dependencies from the build process.
      - deploy_dev_branches: # job #3 - in some cases we want to deploy to dev/prod-like environment
          context: dev
          requires:
            - test
          filters:
            branches:
              ignore:   # This is why we ignore master in this job.
                - master
      - deploy_production_latest: # job #4 - our last job is production “latest”
          context: dev
          requires:
            - test	# we can’t continue to production unless “test” job is finished successfully!
          filters:
            branches:
              only:    # We deploy latest only in master
                - master

CircleCI with NPM token

There are 2 main ways passing private and secure NPM token to each one of our builds. The first one are ENV vars which can be found in the project settings under “environment variables” (click “add” and add “NPM_TOKEN” as key with the value of your npm token). Another better option would be to define context environment variables.

With ENV vars context, once you provide a context and added your ENV vars, you’ll need to add “context: <context_name>”.

// … few jobs

workflows:
  version: 2.1
  build-test-and-deploy:
    jobs:
      - build: # job #1 - here we build and prepare everything we need 
          context: dev
      - test: # job #2 - is where we run our tests, linting, code coverage etc.
          context: dev	# for each job we can define “context” with environment variables (and define those in CircleCI dashboard as admin)
          requires:
            - build	# Before we can continue, we need the “build” job to finish, our tests depend on dependencies from the build process.
      - deploy_dev_branches: # job #3 - in some cases we want to deploy to dev/prod-like environment
          context: dev
          requires:
            - test
          filters:
            branches:
              ignore:   # This is why we ignore master in this job.
                - master
      - deploy_production_latest: # job #4 - our last job is production “latest”
          context: dev
          requires:
            - test	# we can’t continue to production unless “test” job is finished successfully!
          filters:
            branches:
              only:    # We deploy latest only in master
                - master

Understanding “Persist to Workspace”

Under “steps” in your CircleCI configuration you’ll be able to define which folder you want to persist and keep for the next jobs, Saving time during the build process since we already have those folders from previous project (e.g “node_modules” and “dist” folders).

   - persist_to_workspace:
          # Must be an absolute path, or relative path from working_directory. This is a directory on the container which is 
          # taken to be the root directory of the workspace.
          root: ~/app
          # “Paths” Must be relative path from root
          paths:
            - node_modules  # folder we want to keep for the next job
            - packages/my_project/build	# another folder

Above diagram illustrates how we create node_modules at the first job and the folder persist to the next and the third job. Same goes for “dist” folder (note: diagram is not corresponding to the above code, if it was, we would have “dist” starting from job #1).

How to use circleCI Orbs

In CircleCI there are 3rd party plugins that we can use for automation (here’s a list of them) to ease the integration with 3rd parties like AWS. Rather than writing your own integration code you can just use Orbs.

version: 2.1

orbs:
  aws-s3: circleci/aws-s3@1.0.11 # allows easy integration with AWS s3, require ENV var with AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY
  slack: circleci/slack@3.3.0	# allows easy integration with slack, require SLACK_WEBHOOK (url provided by slack)


jobs:
  # Job #1
  build:
    docker:
      - image: circleci/node:11.10
    working_directory: ~/app
    steps:
	# few steps here (run/command)
      - slack/status:
          success_message: Hey @${CIRCLE_USERNAME}, job is done successfully! :)
          failure_message: Hey @${CIRCLE_USERNAME}, job failed! :(
          only_for_branches: master

Advanced Full Example

Our final advanced example will showcase asset pipeline with AWS s3, slack for fail/success job(deploy) messages, unit tests and more. Please read the comments for more information.

The process/jobs

  1. Run the “build” job which meant to prepare our ENV for the next jobs. e.g install dependencies, build the assets with webpack.
  2. Run the “test” job which contain code quality assurance like unit tests, integration, code coverage and more.
  3. Finally either run the deployment to a development branch specific bucket or to production which we call “latest”/”master”
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
version: 2.1

orbs:
  aws-s3: circleci/aws-s3@1.0.11
  slack: circleci/slack@3.3.0

jobs:
# Job #1
  build:
    docker:
      - image: circleci/node:11.10
    working_directory: ~/app
    steps:
      - checkout
      # cache dependencies (to speed automation process)
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "package.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-
      - run:
          name: Authenticate with registry
          command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/app/.npmrc
      - run:
          name: Install NPM dependencies - via yarn
          command: yarn install
      - run:
          name: Install Jest Global
          command: yarn global add jest
      - run:
          name: Build Production Assets
          command: yarn run build
      - persist_to_workspace:
          # Must be an absolute path, or relative path from working_directory. This is a directory on the container which is 
          # taken to be the root directory of the workspace.
          root: ~/app
          # Must be relative path from root
          paths:
            - node_modules  # contain dependencies 
            - dist          # contain files we've build for distribution 
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}

# Job #2
  test:
    docker:
      - image: circleci/node:11.10
    working_directory: ~/app
    steps:
      - checkout
      - attach_workspace:
        # Must be an absolute path or relative path from working_directory
          at: ~/app
      - run:
          name: Test
          command: yarn test
      - run:
          name: Generate code coverage
          command: npm run test -- --coverage

# Job #3
  deploy_dev_branches:
    docker:
      - image: circleci/node:11.10
    working_directory: ~/app
    steps:
      - checkout
      - attach_workspace:
        # Must be an absolute path or relative path from working_directory
          at: ~/app
      - run:
          name: apt-get update
          command: sudo apt-get update
      - run:
          name: Install python-dev
          command: sudo apt-get install python-dev
      - run:
          name: Install pip
          command: sudo apt-get install python-pip
      - run:
          name: Install awscli
          command: sudo pip install awscli
      - run:
          name: "What branch am I on?"
          command: echo ${CIRCLE_BRANCH}
      - run:
          name: "What AWS region i am on now?"
          command: echo $AWS_REGION
      - aws-s3/sync:
          from: ~/app/dist/my-project
          to: 's3://bucket-assets/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BRANCH}/${CIRCLE_SHA1}'
          arguments: |
            --region $AWS_REGION \
            --acl public-read
# Job #4
  deploy_production_latest:
    docker:
      - image: circleci/node:11.10
    working_directory: ~/app
    steps:
      - checkout
      - attach_workspace:
        # Must be an absolute path or relative path from working_directory
          at: ~/app
      - run:
          name: apt-get update
          command: sudo apt-get update
      - run:
          name: Install python-dev
          command: sudo apt-get install python-dev
      - run:
          name: Install pip
          command: sudo apt-get install python-pip
      - run:
          name: Install awscli
          command: sudo pip install awscli
      - run:
          name: "What branch am I on?"
          command: echo ${CIRCLE_BRANCH}
      - run:
          name: "What AWS region i am on now?"
          command: echo $AWS_REGION
      - aws-s3/sync:  # first copy per commit hash (easier to locate files)
          from: ~/app/dist/my-project
          to: 's3://bucket-assets/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BRANCH}/${CIRCLE_SHA1}'
          arguments: |
            --region $AWS_REGION \
            --acl public-read
      - slack/status:
          success_message: Hey @${CIRCLE_USERNAME}, job is done successfully! :)
          failure_message: Hey @${CIRCLE_USERNAME}, job failed! :(
          only_for_branches: master

workflows:
  version: 2.1
  build-test-and-deploy:
    jobs:
      - build: # job #1
          context: dev
      - test: # job #2
          context: dev
          requires:
            - build
      - deploy_dev_branches: # job #3
          context: dev
          requires:
            - test
          filters:
            branches:
              ignore:   # We ignore master/latest here (this is branch deployment)
                - master
      - deploy_production_latest: # job #4
          context: dev
          requires:
            - test
          filters:
            branches:
              only:    # We deploy latest only in master
                - master

Tips & Recommendation

Debugging Machine

If a job fails, you don’t have to rerun the job from scratch. Just rerun job with SSH, copy the SSH command into your terminal and debug the job at the point of failure (re-run the commands inside the machine and debug the problematic steps).

Remember to cancel job when you’ve done with the machine.

Multi Config Projects

In multi projects automation configuration it would be easy for you to write the jobs and host them at some external registry (like npm, Have npm package for each job and download/install and execute the code) or external shell script so you can control all jobs across repos. So you’ll have something like “@startup/pre-deploy” package or “@startup/post-deploy” or job specific package like “@startup/ci-run-tests”, “@startup/ci-run-deployment-prod” etc.

Doing so you’ll be able to upgrade the job process remotely and cross repos with ease.

Speed is important

How fast your deployment process is important, it should be derived from the architecture of your application (building modular code/apps, small chunk of repos/code. Not one big monolith project. But of course it depends on your project needs).

To improve CI performance

  1. Use your own docker image with as many dependencies pre-installed.
  2. Use “persist_to_workspace” feature by circle CI to jump between projects without reinstalling everything from scratch.
  3. Work with “restore_cache” and “save_cache” to cache folders like node_modules or any dependencies folder.

Summary

Automation and CI are powerful tools for managing many team of 2–3 developers per team with ease and keeping fast and “waterfall” development process. The overall cost of settings & configuration is low and maintaining such workflow reduces bugs, speeds up development, reduces overall stress on developers (no “big releases” anymore) and visualized development progress for team leads/managers/project managers overseeing projects.

Leave a Reply

Your email address will not be published. Required fields are marked *

All rights reserved 2024 ©