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:
-
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.
-
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 thedirectories
variable. -
it ensures all configuration files exist & are up to date. This step uses ansibles template directive and can be configured using the
templates
variable. -
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.
-
a production bundle is created to make sure that all production dependencies are installed properly.
-
the database is migrated and the assets are generated. Both steps are configurable using the
migrate
andcompile_assets
variable, and default totrue
-
if all prior steps succeeded, the deployment is considered a success, and the current symlink is updated to the new location.
-
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