Elixir and continuous delivery with Circle CI

Elixir and continuous delivery with Circle CI

In an earlier article (Blazing fast CI suite for Elixir on Circle CI 2.0) I wrote about how we could set up Circle CI to have a faster pipeline by reusing compiled resources in multiple steps but intentionally kept out the part about deploying to an environment. This is what we’ll explore today now that the release of Distillery 2 has made things so much easier for us.

Assuming that we already have done the steps that does testing, linting, type checks and so on we’ll have to figure out what we’ll need to do in order to deploy, these are the steps:

  1. Make sure we have the correct erlang/elixir version
  2. Check out the repository
  3. Install hex and rebar locally
  4. Fetch and compile the projects dependencies
  5. Build assets (in case we’re running phoenix app or anything else that requires JS/CSS)
  6. Build the release
  7. Upload, unpack the release on the actual server
  8. Restart or do a hot code reload

Those steps together with some caching ended up looking like this:

deploy_steps: &deploy_steps
  steps:
    - checkout
    - restore_cache:
        keys:
          - v1-deps-cache{{ checksum "mix.lock" }}
          - v1-deps-cache
    - run: mix local.hex --force
    - run: mix local.rebar --force
    - run: mix deps.get
    - run: mix deps.compile
    - save_cache:
        key: v1-deps-cache{{ checksum "mix.lock" }}
        paths:
          - _build
          - deps
          - ~/.mix
    - run: mix compile
    - restore_cache:
        keys:
          - v1-js-deps-cache{{ checksum "assets/package-lock.json" }}
          - v1-js-deps-cache
    - run: cd assets && npm install
    - save_cache:
        key: v1-js-deps-cache{{ checksum "assets/package-lock.json" }}
        paths:
          - assets/node_modules

    # node-sass had to be rebuilt when running centOS 
    - run: cd assets && npm rebuild node-sass
    - run: cd assets && node_modules/brunch/bin/brunch build --production
    - run: mix phx.digest
    - run: mix release
    - run:
        name: Move the release to the server
        command: APP_VERSION=`bin/app_version` && scp -o
StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
$BUILD_PATH/$APP_VERSION/$APP_NAME.tar.gz
deploy@$SERVER_IP:$APP_PATH/releases/$APP_NAME-$APP_VERSION.tar.gz
    - run:
        name: Unpack release
        command: APP_VERSION=`bin/app_version` && ssh -o
StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null deploy@$SERVER_IP tar
-xf $APP_PATH/releases/$APP_NAME-$APP_VERSION.tar.gz -C $APP_PATH/
    - run:
        name: Run migrations
        command: ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
deploy@$SERVER_IP $APP_PATH/bin/$APP_NAME migrate
    - run:
        name: Restart service
command: ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t
deploy@$SERVER_IP sudo systemctl restart $SERVICE_NAME

Notice that there are some somewhat secret things going on in here.

First is that Erlang, elixir nor node is installed. That is because circle is running a custom docker image that includes all them already.

Another thing is that APP_VERSION is fetched from a bin stub called app_version. That is actually a small helper to parse and get the version that is set in the mix file. The contents can be seen here: Script to get the current version from an elixir mix file without the need for elixir · GitHub.

There is also a bunch of environment variables being used that aren’t set. The reason for this is that these deploy steps is supposed to be reused (see &deploy_steps in the top of the code block).

In order to set the before mentioned environment variables we’ll use another reusable block. We’ll also define which docker image we’d like to use (can be optional, but is recommended since you’d otherwise have to build and cache all build dependencies by hand). This is an example of my configuration:

prod_defaults: &prod_defaults
  working_directory: ~/mintcore-elixir
  docker:
    # Cent OS image with erlang, elixir and node bundled with a prebuilt
dialyzer PLT
    - image: johantell/elixir-plt-node:1.7.3
      environment:
        APP_NAME: mintcore
        APP_PATH: /path_to_where_my_app_is_installed/mintcore.se
        BUILD_PATH: _build/prod/rel/mintcore/releases
        MIX_ENV: prod
        SERVER_IP: mintcore.se
      SERVICE_NAME: mintcore.se

After that it’s really easy to define delivery to multiple environments as all you’d need is to add this to your jobs:

jobs:
  # ...
  deploy_production:
    <<: *prod_defaults
    <<: *deploy_steps
  # ...

Note that there are a few assumptions going on here:

  • The build server has ssh access to the environment it should deploy to (you can add these in circle CI settings)
  • You are using distillery 2.0 and have a runtime configuration on your server.
  • Your app is currently running on your server and can be handled via systemctl