Continuous Deployment

Goals

  1. master branch should always reflect what’s in production.
  2. Stage and production environments should be the same with the exception of data.
  3. Merging a branch into master will deploy the latest code without need for human interaction.
  4. End users should never be negatively impacted when new code is deployed.
  5. Deployments should happen in minutes and as many times per week/day/hour as necessary.
  6. We’re developers by trade. This has to be as low-maintenance as possible.

Prerequisites

The Build

  1. Push to a feature branch
  2. Open a pull request into master
  3. Build server runs tests and static code analysis on the feature branch
  4. If the build passes and the code review goes well, merge into master and delete the feature branch
  5. Build server runs tests and static code analysis on master
  6. If the build on master passes, deploy to stage and production environments
  1. Spin up a container with the appropriate environment set up for running the app
  2. Install dependencies
  3. Checkout the branch in question
  4. Run the test suite (for us it’s rspec spec or yarn test)
  5. Report test coverage to static code analysis tool
  6. Tell Github whether tests passed or not
defaults: &defaults  # our repo...get it? dont_fear, the repo...MORE COWBELL!
working_directory: ~/angelMD/dont_fear
parallelism: 1
shell: /bin/bash --login
environment:
CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
AWS_DEFAULT_REGION: us-west-2
TERRAFORM_VER: 0.9.6
PATH: $PATH:$HOME/.local/bin:$HOME/bin
CC_TEST_REPORTER_ID: some_id
ELASTICSEARCH_VERSION: 5.3.3
TZ: "/usr/share/zoneinfo/America/Denver"
docker:
# our own base container with some dependencies baked in to speed up builds
- image: angelmd/api_test_image
auth:
username: $DOCKER_USER # stored in circleci
password: $DOCKER_PASS # stored in circleci
command: /sbin/init
environment:
BASH_ENV: /root/.bashrc
TZ: "/usr/share/zoneinfo/America/Denver"
version: 2
jobs:
build:
<<: *defaults
steps:
- run: echo 'export PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' > $BASH_ENV
- run: echo $PATH
- checkout
- run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS
- run:
working_directory: ~/angelMD/dont_fear
command: 'echo ''America/Denver'' | tee -a /etc/timezone; dpkg-reconfigure -f noninteractive tzdata; service postgresql restart; '
- run: gem install bundler -v 1.15.1
- run: echo -e "export RAILS_ENV=test\nexport RACK_ENV=test" >> $BASH_ENV
- restore_cache:
keys:
- gem-cache-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
- gem-cache-{{ checksum "Gemfile.lock" }}
- run: 'bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3 '
- save_cache:
key: gem-cache-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
paths:
- vendor/bundle
- run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- run: chmod +x ./cc-test-reporter
- run:
name: Running Elasticsearch
command: service redis-server start
background: true
- run:
name: Running Redis
command: /bin/su - elasticsearch -c "/elasticsearch-5.3.3/bin/elasticsearch -d"
background: true
- run: wget --waitretry=5 --retry-connrefused -v http://127.0.0.1:9200/
- run: mv config/database.build.yml config/database.yml
- run:
command: bundle exec rake db:create db:schema:load --trace
environment:
RAILS_ENV: test
RACK_ENV: test
- run: bundle exec rake db:seed
- run: mv config/application.yml.test config/application.yml
- run: ./cc-test-reporter before-build
- run: bundle exec rspec; ./cc-test-reporter after-build --exit-code $?
- store_test_results:
path: /tmp/circleci-test-results
- store_artifacts:
path: /tmp/circleci-artifacts
deploy:
<<: *defaults
steps:
- run: echo 'export PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' > $BASH_ENV
- run: echo $PATH
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run: bin/build.sh
- run: bin/push.sh
- run: bin/deploy.sh
workflows:
version: 2
test-deploy:
jobs:
- build
- deploy:
filters:
branches:
only: master
requires:
- build

Frontend Push & Deploy

Backend Push & Deploy

The Push

HASH=$(git rev-parse HEAD)rm ./versionecho $HASH > ./versiondocker build --rm=false -t angelmd/app_name .
set -euo pipefail
IFS=$'\n\t'
docker login -u $DOCKER_USER -p $DOCKER_PASS
REMOTE=angelmd
NAME=app_name
HASH=$(git rev-parse HEAD)
docker tag $REMOTE/$NAME $REMOTE/$NAME:$HASH
docker push $REMOTE/$NAME:$HASH
docker tag $REMOTE/$NAME $REMOTE/$NAME:latest
docker push $REMOTE/$NAME:latest
docker logout

The Deploy

export PATH="$PATH:/root/.local/bin"
export TF_VAR_docker_tag=$(git rev-parse HEAD)
export PATH="$PATH:"
cd dont-fear-service
. stage_env
terraform apply -var-file=dont-fear-service.tfvars
. prod_env
terraform apply -var-file=dont-fear-service.tfvars

YES!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store