A few months ago, we faced the challenge of moving a large website to a new hosting provider, in order to reduce costs as well as increase reliability and performance. After looking at a variety of options, we settled on Linode as the best fit for the site, and set about planning the migration.
We’d provisioned previous sites on Linode with our Stackscript, but this was getting out of date and designed more for single-Linode hosting, rather than the 4-Linode configuration we wanted for this project. We thus decided to take a look into the current crop of server provisioning frameworks – and, having tried out both Puppet and Chef in the past, I was keen to try out Ansible.
For our use case – provisioning a small set of servers on Linode – I found that Ansible worked pretty well. It strikes a good balance between being declarative and imperative, meaning that you don’t have to wrap your head around a dependency graph whilst writing a playbook – in Ansible you can only depend on handlers
(via notify
), which is generally used to restart services and covers 80% of what you need it for. Otherwise your tasks’ dependencies are simply the tasks that happen before them (hence imperative).
Although Ansible itself is written in Python, you do all your task-writing in YAML (something that should be familar to Rubyists), and doesn’t require anything to be installed on the target server(s).
I also like Ansible’s ad-hoc commands, which allow you to use a single module for a one-off task (like looking at a file, running a shell command, doing a database backup) across multiple servers.
We stuck with using Capistrano for deployments (rather than going the whole way and using Ansible for this as well), with a Ansible playbook to setup the basic directory structure for Capistrano. This also fitted well with our use of Codeship for automated deployments from git pushes.
Tips
Look at existing open-source playbooks for inspiration
Generally good advice for any type of code! I found the following GitHub repos useful for ideas/inspiration when writing playbooks:
I’d also recommend the following Ansible Galaxy roles:
- franklinkim.sudo
- rvm_io.rvm1-ruby
- geerlingguy.nginx
- geerlingguy.mysql
- nickhammond.logrotate
- geerlingguy.elasticsearch
- geerlingguy.nfs
You may notice the username of Jeff Geerling in there a few times – his GitHub profile has a lot of well-written roles (all available on Ansible Galaxy), which strike a good balance between doing a minimal installation and trying to do too much or being too opinionated. Thanks Jeff!
Use tags
Various guides I’ve read on the subject encourage you to make liberal use of tags, and this is something that proves very useful. Although you can run an entire playbook and generally have it not do anything unneeded (idempotence and all that), being able to run a subset of your playbook for quick tasks like updating an ENV variable is a nice thing to have.
Use separate host files for production and staging
Yes, it’s another flag (-i
) to add to all your commands, but the separation of the two environments – and the piece of mind this brings – is well worth a few more keystrokes.
Storing secrets
Since I – and the whole team – were new to Ansible, I consciously decided to not to use Ansible Vault for storing “secrets” (passwords, API keys etc.), since it would have been another thing to remember to have re-encrypt the vault before committing changes in git. Instead, I opted to keep all secrets in a single file, which was gitignore-d, with environment-specific keys stored in a hash keyed by the name of the environment (like the database.yml
file in Rails).
The Ansible docs tell you (at the time of writing) that you can have a file called all.yml
in your group_vars
directory that will be available to all hosts – but not mentioned is the fact that you can also have a directory named all
, with all YAML files inside it loaded for all hosts. This means you can split your secret and not-secret configuration into two files (I used config.yml
and secrets.yml
), and be able to just have to deal with the one file, which we can then securely distribute to anyone working with the repo.
Github API for SSH keys
Not so much an Ansible-specific tip, but you can get a GitHub user’s public keys by appending .key
to their profile URL like so: https://github.com/username.keys
. Since the Ansible authorized_key
module understands URLs, we can construct a loop over the GitHub usernames of the members of our team:
- name: Add authorized keys
authorized_key:
user: rails
key: "https://github.com/.keys"
with_items:
- github_user_1
- github_user_2
Dry run diff mode
Particularly valuable for making ad-hoc changes to config files, Ansible’s dry run mode can be activated with the --check --diff
flags – the --check
bit activates the dry run mode, the --diff
mode (unsurprisingly) provides a diff of your changes.
A few things to watch out for: anything that relies on variables captured by register
will fail, since those tasks won’t be run. Secondly, Python hashes (dictionaries) don’t have a defined iteration order (like they do in Ruby post-v1.9), so your some of your diffs of iterated hashes (for example, setting ENV variables in a .env
file) will have extraneous lines where things have been rearranged.
Conclusion
Yes, it doesn’t have a “pull” mode (you just have to make sure all your servers are in sync), and the limitations of YAML not actually being a programming language do grate at times, but for managing a handful of servers, Ansible has proved to be a valuable addition to our sysadmin-ing arsenal.