December 28, 2013

deploying rails applications with ansible

Fellow web developers might know ansible - a lightweight, extensible solution for automating your application provisioning.

Since ansible galaxy opened its doors sharing roles became very easy which is why I started sharing the deployment and rollback roles I’m using for some Ruby on Rails projects I’m working on.

First, some thoughts on why I switched deployments from mina to ansible:

development of mina stagnated over the past year

Mina still lacks some important features like rollbacks, while other features like multi host deployments are only possible through hacks. Since most pull requests aren’t accepted by the maintainers of mina this situation didn’t improve at all.

Most of the hacks which are necessary when working with mina are not necessary when using ansible.

Now ansible is much harder to debug as soon as you start extending it with custom library modules, but most users probably won’t reach this situation until they’ve spent some time with ansible due to the huge amount of modules already available. To me this is a much better situation to work with.

deploying Ruby on Rails applications with ansible

Looking at capistrano and mina most deployments consist of only three steps:

  • checking out a new version of your application
  • applying changes to your environment (database migrations, assets, …)
  • restarting the application server to pick up code changes

Depending on your application every step can involve some complex operations, like zero downtime deployments with dropped database columns in between. These more complex problems require special handling and you need to take care of this.

ansible-rails-deployment executes the first two steps and also handles some additional setup which might be required:

  1. it takes care of a proper $GEM_HOME and $PATH setup. This is important because all necessary binaries (bundle, gem, …) need to be locatable using $PATH, otherwise ansible won’t find the binaries.

  2. then it makes sure that all necessary folders exist, like {{ deploy_to }}, {{ deploy_to }}/shared, {{ deploy_to }}/releases, and all other folders you might need. This can be adjusted using the directories variable.

  3. it ensures all configuration files exist & are up to date. This step uses ansibles template directive and can be configured using the templates variable.

  4. now that the pre-requirements are met, a bare copy of your git repository is created or updated, and then cloned into a separate build directory.

  5. a production bundle is created to make sure that all production dependencies are installed properly.

  6. the database is migrated and the assets are generated. Both steps are configurable using the migrate and compile_assets variable, and default to true

  7. if all prior steps succeeded, the deployment is considered a success, and the current symlink is updated to the new location.

  8. lastly, old versions of your code are removed. only the 5 most recent versions are kept.

The application restart needs to be handled in a separate role, which you write yourself, because ansible does not support dynamic (notify-)handler invocation.

rolling back Ruby on Rails deployments with ansible

ansible-deployment-rollback only needs to change the symlink of the current release to the prior release and restart the application. Assuming all other side-effects are taken care of roles you write yourself.

Thus, a typical, minimal deployment playbook, which supports rollbacks as well, looks like this:

---
- hosts: server
  user: app-user
  gather_facts: False
  vars:
    user: app-user
    home_directory: "/home/{{ user }}"
    rails_env: "staging"

  roles:
    -
      role: nicolai86.ansible-rails-deployment

      repo: [email protected]:awesome-app
      branch: master

      deploy_to: "{{ home_directory }}/app"

      symlinks:
        -
          src: "{{ shared_path }}/log"
          dest: "{{ build_path }}/log"
        -
          src: "{{ shared_path }}/config/database.yml"
          dest: "{{ build_path }}/config/database.yml"
        -
          src: "{{ shared_path }}/vendor/bundle"
          dest: "{{ build_path }}/vendor/bundle"

      directories:
        - "{{ shared_path }}/log"
        - "{{ shared_path }}/config"
        - "{{ shared_path }}/vendor"
        - "{{ shared_path }}/vendor/bundle"

      templates:
        -
          src: "templates/config/database.js"
          dest: "{{ shared_path }}/config/database.yml"

      tags: deploy

    -
      role: nicolai86.ansible-deployment-rollback

      deploy_to: "{{ home_directory }}/app"

      tags: rollback

    -
      role: restart

      service: my-app:*

      tags:
        - deploy
        - rollback

This takes care of simple deployments for you, and also integrates nicely with your existing ansible provisioning.

Using tags you can either deploy, or rollback. Just make sure to specify the correct tag.

When deploying, as a small bonus, migration and asset compilation only takes place if there are differences between the current and next version (this is checked using the diff command, just like mina does).

If deployments require special handling (like zero downtime deployments with loadbalancers in between) you can easily fit that in your restart roll, or add a new roles prior/ posterior to the deployment.

The same goes for rollbacks: run your down-migrations in a separate role prior to restarting, and everything is fine.

This leaves us with a simple, and extensible group of roles which can ease deployments and rollbacks - no matter how complicated things get.

2014/02/12 take a look at this gist for a current usage example

© Raphael Randschau 2010 - 2022 | Impressum