Deploying Rails Applications with Ansible

December 8, 2013

Push based deployments and why Ansible

A few days ago I was discussing with Itamar Hassin, a ThoughtWorker friend of mine, about the current status of Configuration Management (CM) tools regarding arbitrary operations like simple application deployments, one-time backups, etc.

These kinds of operations do not fit very well with pull-based strategies, where we have agents on managed hosts pulling and applying configuration from a canonical source.

Puppet and Chef support arbitrary operations via puppet apply and chef-solo respectively, but both would need something like Capistrano or Fabric to drive them.

So here comes Ansible! Ansible is a new player that promises some interesting features and one of them is exactly to eliminate the need for an additional layer of complexity when doing push based workflows.

Read more about pull vs push strategies to understand better what we are talking about.

The objective of this post is to demonstrate how to use Ansible to:

Pre-requisites

Provisioning and Deployment with Ansible

$ git clone https://github.com/jeffersongirao/deploying_rails_with_ansible

$ cd deploying_rails_with_ansible

$ vagrant up

The step above will take some time depending on your internet connection. It will:

To deploy you need first to configure the file deployment/config.yml to match the details of your application. After that, just run:

$ ansible-playbook deployment/deploy.yml -i vagrant_ansible_inventory -u vagrant --private-key=~/.vagrant.d/insecure_private_key

If the execution was successful you should have the application running on http://localhost:10080.

Issues and decisions

Ansible uses YAML to define the configuration state, it is a terse and readable syntax so no need to explain it line by line. However, I think it is interesting to note some of decisions made and pain points faced.

Rbenv and the gem module

# provisioning/app_server.yml
- name: install required gems
  gem: name={{item}} state=present user_install=no
  with_items:
    - bundler
    - passenger

The critical part here is the user_install flag as it contradicts what we are doing actually in the playbook. By default the Ansible’s gem module installs the ruby gem in the user space but not where rbenv expects it. It calls the gem install with the flag --user-install that places it in the user’s home directory instead of GEM_HOME, the place used by rbenv. This is not an issue with Ansible, just a discrepancy in how the gem and rbenv commands work.

Where should we put sensitive data?

Should we store sensitive data like API keys as plain text in the script? It depends a lot on your application profile, team and your level of paranoia. There are some initiatives around a secure vault mechanism for Ansible being discussed but no built-in implementation is around yet. There are people using git-crypt for this purpose too.

Another option is to enter the data via playbook prompts if you want to manage it outside of the app’s source control.

If the point is just to get a random password set in a idempotent way the password lookup plugin is useful. It is the one that we used for setting the database password as bellow:

# deployment/config.yml
db_password: "{{lookup('password', 'credentials/postgresql length=15')}}"

This approach has the obvious requirement of running the playbooks from a machine that have access to the password storage.

Hanging on git clone

Ansible was designed for non-interactive operations so any prompt will hang the execution. Double check if the host with your git repo is on ~/.ssh/known_hosts and also ensure there is no keyphrase in your ssh keypair. You can use the expect command for the cases where you cannot avoid providing some input.

# deployment/deploy_app.yml
- name: ensure bitbucket.org is part of known hosts
  lineinfile: >
    dest={{user_home}}/.ssh/known_hosts
    line="bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw=="
    regexp=bitbucket.*
    state=present
    create=yes

The purpose of known_hosts is to avoid Man-in-the-middle attacks, by preventing connection when the signature changes unexpectedly, a sign that an attack may be happening. Note that we are adding the signature for bitbucket in a static way. Some examples use the direct output of ssh-keyscan hostname what I would not suggest as it eliminates the benefit of known_hosts verification.

What about real infrastructure?

The scripts should work with little or no change anywhere you are deploying to. You can, for example, spin up Ubuntu machines on a Cloud service and the only necessary change should be an inventory file pointing to them.

We could even spin up these machines there with Vagrant and generate the inventory automatically to be used with Ansible. A topic for the next blog post, stay tuned :)

Discussion, links, and tweets

I'm a developer and Continuous Delivery/DevOps enthusiast at ThoughtWorks Inc.
Follow me on Twitter; I regularly post some random thoughts there



comments powered by Disqus