Home >

Mixing Ansible Roles and Tasks

27 Jun 2015

Over the past couple of weeks I've been working on refactoring Ansible deploy scripts for a client where we are deploying several Rails based services.

Most of this refactor has been pulling common tasks like setting up unicorn and nginx into distinct roles. Here is a condensed example playbook:

---
- hosts: all

  pre_tasks:
    - name: upgrade packages
      apt: upgrade=yes update_cache=yes cache_valid_time=300
      sudo: yes

  roles:
    - { role: brightbox_ruby, ruby_version: 2.2.2 }
    - { role: deploy_code,
        git_branch: master,
        repo: 'https://github.com/calebwoods/myapp',
        ruby_env_template: 'templates/production_env.rb' }
    - { role: unicorn }
    - { role: nginx, nginx: { app: 'myapp' } }
    - { role: sidekiq, apps_dir: '/var/www' }

  post_tasks:
    - name: setup log rotation
      sudo: yes
      template: src=templates/logrotate.conf dest=/etc/logrotate.d/myapp.conf

In this playbook there are a few pre_tasks which will run before all the roles. This is useful to get the state of the machine ready for the rest of the playbook such as updating the base packages or installing specific application dependencies. post_tasks or tasks can be used for after the roles have run for similar types of tasks.

Interleaving commands

The challenge when refactoring these playbooks was where to put commands like bundle install or rake db:migrate. These are fairly common and require that prerequisite roles have been executed, such as Ruby being installed. In addition, later roles like, unicorn and sidekiq, require that those commands have already been run. Typically these would be done with tasks, but Ansible only gives you the option of running tasks before or after roles.

Additionally, some applications will have rake tasks or other one off commands that need to be run inbetween roles such setting up Cron jobs with whenever, bundle exec whenever --update-cron. So for these reason it would be nice if there was a general solution rather than having to create a new role for every special case.

Simple solution

The solution I came up with was to create an Ansible role for running one off bash commands, called bash_command.

The role has two simple inputs: command and dir. command is the string you want to run and can include interpolated Ansible variables. The command will be using bash -lc through the shell module to run the command which means it runs in a login bash shell, so you will have access to any ENV variables that have been set elsewhere in your playbook. The dir input specifies where to cd into before running the command.

Here is an example of how the roles section of my playbook would be structured for a Rails application using bundler, whenever, database migrations, and precompiling assets:

roles:
  - { role: brightbox_ruby, ruby_version: 2.2.2 }
  - { role: deploy_code,
      git_branch: master,
      repo: 'https://github.com/calebwoods/myapp',
      ruby_env_template: 'templates/production_env.rb' }
  - { role: bash_command,
      command: 'bundle install --binstubs bin --without development test --deployment --path vendor/bundle',
      dir: '/var/www/myapp' }
  - { role: bash_command,
      command: 'bundle exec whenever --update-cron',
      dir: '/var/www/myapp' }
  - { role: bash_command,
      command: 'bundle exec rake db:migrate',
      dir: '/var/www/myapp' }
  - { role: bash_command,
      command: 'bundle exec rake assets:precompile assets:clean',
      dir: '/var/www/myapp' }
  - { role: unicorn }
  - { role: nginx, nginx: { app: 'myapp' } }
  - { role: sidekiq, apps_dir: '/var/www' }

Have other tricks you've used to create more reusable Ansible scripts? Tweet at me @calebwoods.