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
- Bugs can be fixed immediately based on urgency.
- Cross teams & cross feature code alignment helps preventing integration conflicts and bugs.
- 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.).
- Early collection of metric data (e.g, how many times HTTP requests will be made for a non-existing feature).
- Features need to be “hidden” (with feature flag or hardcoded toggle, preferably feature flag, so you could turn off buggy feature without deployment/revert).
- 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
- Unit tests.
- Integration tests.
- Performance benchmarks.
- Codeclimate/codecov.
- Code linting (csslint, eslint).
- Security detectors.
- Commit msg guidelines (e.g start commit with [FEAT/BUG/FIX]).
Deployment
- Per env deployment (production, development, production like, staging etc).
- Static assets deployment.
- Static documentation sites.
- Package publication (npm publish).
Commutation
- Organization slack webhook (to know when someone push to master).
- 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).
- Automated PR/git issue for consumer of a package.
- Automated documentation generator.
Scripts
- Update internal servers (via curl or having external API).
- Deleting files.
- 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
- Run the “build” job which meant to prepare our ENV for the next jobs. e.g install dependencies, build the assets with webpack.
- Run the “test” job which contain code quality assurance like unit tests, integration, code coverage and more.
- 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
- Use your own docker image with as many dependencies pre-installed.
- Use “persist_to_workspace” feature by circle CI to jump between projects without reinstalling everything from scratch.
- 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.
Lior Amsalem embarked on his software engineering journey in the early 2000s, Diving into Pascal with a keen interest in creating, developing, and working on new technologies. Transitioning from his early teenage years as a freelancer, Lior dedicated countless hours to expanding his knowledge within the software engineering domain. He immersed himself in learning new development languages and technologies such as JavaScript, React, backend, frontend, devops, nextjs, nodejs, mongodb, mysql and all together end to end development, while also gaining insights into business development and idea implementation.
Through his blog, Lior aims to share his interests and entrepreneurial journey, driven by a desire for independence and freedom from traditional 9-5 work constraints.
Leave a Reply