Ansible: Server configuration the easy way

Setting up servers manually can be tedious and error-prone. Therefore, we used Chef1 at our company2 for defining the server layout and configuration in one place. But learning Chef was not a very easy and straight forward process and new team members are facing a steep learning curve. Therefore, when learning about Ansible 3 as a very easy to use alternative, I was happy to try it out. Now, I completed the conversion of one of our main install scripts from Chef to Ansible. Today, I want to show you, how you can start using Ansible after some minutes.

Whenever playing with servers, using Virtual machines is a good idea. So, install VirtualBox and Vagrant first.

Vagrant Setup

If you do not know about Vagrant: That is a very easy to use way to script virtual machines for fast creation of new boxes over and over again. It is strongly recommended, when starting with server provisioning and testing.

Install VirtualBoxor VMWare instead, which might also work and Vagrant4. Check if there are newer versions, than used in the snippet below.

wget http://files.vagrantup.com/packages/b12c7e8814171c1295ef82416ffe51e8a168a244/vagrant_1.3.1_x86_64.deb
sudo dpkg -i vagrant_1.3.1_x86_64.deb

cd somedir
vagrant init

Vagrant init will create a Vagrantfile. You should read and modify it, before downloading and starting the VM. Here is my current one:

# Vagrantfile
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise32"
  config.vm.box_url = "http://files.vagrantup.com/precise32.box"
  # config.vm.network :private_network, ip: "192.168.111.222"
  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "vagrant.yml"
  end

  config.vm.network :forwarded_port, guest: 8080, host: 3751
  config.vm.network :forwarded_port, guest: 80, host: 3750
  # config.vm.network :private_network, ip: "192.168.33.10"
  # config.vm.network :public_network

  # config.ssh.forward_agent = true

  # Share an additional folder to the guest VM. The first argument is
  # the path on the host to the actual folder. The second argument is
  # the path on the guest to mount the folder. And the optional third
  # argument is a set of non-required options.
  # config.vm.synced_folder "../data", "/vagrant_data"
  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id, "--memory", "1024", "--name", "ansible-plaything"]
  end
end

I configure this particular box to use a Ubuntu precise 32bit image and enable ansible by pointing it to a (not yet existing) vagrant.yml. Furthermure, I forward 2 ports into the VM to be able, to test the webapp inside. I also increase the RAM size to 1gig (VirtualBox only).

Start the VM by:

touch vagrant.yml # just create the file, so vagrant does not complain about missing
vagrant up

This should fail in some way, because we didn't create the vagrant.yml yet. Nevertheless, the VM should run in background and you can manually connect with:

vagrant ssh
# or ssh -p 2222 vagrant@localhost
# and PW:vagrant

any time. Besides, the vagrant commands halt, destroy and provision are useful.

Ansible Setup

Ansible requires Python, which should be no deal on any *NIX machine. For Ubuntu and Debian there are packages, of course:

sudo apt-add-repository ppa:rquillo/ansible
sudo apt-get update
sudo apt-get install ansible

For other systems, look up in the Getting started docs

Now, the binarys ansible and ansible-playbook should be available.

Getting started with ansible

As pointed in the Vagrantfile, we should create a vagrant.yml and fill in the first script:

---
# vagrant.yml
- hosts: all
  user: vagrant
  sudo: True
  tasks:
  - name: Update APT package cache
    apt: update_cache=yes

  - name: Install packages
    apt: pkg=$item state=installed
    with_items:
      - bash-completion
      - curl
      - dnsutils
      - fail2ban
      - htop
      - imagemagick
      - iotop
      - liblzma-dev
      - libpcre3-dev
      - mosh
      - openssl
      - pkg-config
      - realpath
      - vim
      - zlib1g-dev
  - name: Copy .bashrc for root
    copy: src=files/bashrc dest=/root/.bashrc owner=root
  -# include: ag.yml

In above yaml:

  • hosts: all is a matcher, to target specific types of servers (more, see under Inventory in the docs).
  • we set the install user to vagrant (instead of root/current user) with sudo.
  • furthermore define 4 tasks:
    • Refresh of packages (read: apt-get update)
    • Installation of some apt-packages. Here, you can see the template syntax with_items, which will loop over all the items in the list and execute the command. A command usally consists of at least 2 parts: a (optional but recommended) name, and the command itself. Here, we reference the apt-module with 2 arguments, a package and a expected state (install the packages ~ possible could also be remove/purge).
    • The second command will copy a file .bashrc to the root server and set the owner to root. To make this run, create a folder/file ./files/bashrc in the project folder and insert your desired settings.
    • Last, we also include another playbook, ag.yml, which will be included into the playbook. First comment it out, it will be used in the next section.

You can create an empty ag.yml or leave it out and install your server:

vagrant provision
# or:
ansible-playbook -i vagrant_ansible_inventory_default --verbose --user=vagrant --private-key=~/.vagrant.d/insecure_private_key vagrant.yml

Vagrant will run some command similar to the second one. This is also the syntax to keep in mind, when deploying that installation to a real machine.

Reuse

Ansible has 2 ways of structuring: inclusions and roles.

Inclusion

Just uncommend/insert the - include: ag.yml line in the playbook and create this:

---
# ag.yml
- name: Install packages for Ag
  apt: pkg=$item state=installed
  with_items: [ automake,pkg-config,libpcre3-dev,zlib1g-dev,liblzma-dev, git, build-essential]
- name: Checkout Ag
  git: repo=https://github.com/ggreer/the_silver_searcher.git
       dest=/usr/local/src/ag
       update=no
- name: Compile Ag
  command: bash build.sh chdir=/usr/local/src/ag
           creates=/usr/local/src/ag/ag
- name: Install Ag
  command: make install chdir=/usr/local/src/ag
           creates=/usr/local/bin/ag

This will install the awesome code search tool Ag directly from github onto the server. This presents a very good example where we:

  1. installing some packages - this time with condensed array syntax
  2. Check out the Git of Ag to /usr/local/src/ag
  3. Run a shell command "build.sh" in this directory. A lot of commands have the argument creates=FILENAME, which will check afterwards if the file was created. It will also skip the whole command, if that file already exists. Note that you can use linebreaks in YAML or write the whole command in one line.
  4. Run make install, again with checking for existing

Roles

The other kind of reuse pattern are roles. Create a folder roles/common/tasks and move the ag.yml to roles/common/main.yml:

mkdir -p roles/common/tasks
mv ag.yml roles/common/tasks/main.yml

Then, instead of including the ag.yml, add a role to the server group:

---
# vagrant.yml
- hosts: all
  user: vagrant
  sudo: True
  roles:
    - common
#...

Our folders and files:

├── files
│   └── bashrc
├── roles
│   └── common
│       └── tasks
│           └── main.yml
├── vagrant_ansible_inventory_default
├── Vagrantfile
└── vagrant.yml

A role can have it's own tasks, templates5, files and handlers6.


Read this gist for a complete overview over all command options, handlers6, files and vars. Also check out the full module reference, which are the built-in commands, like apt, git, mysql and so on.

Conclusion

  • [+] easy to understand, easy to write through yaml
  • [+] very humble requirements: No daemon/dedicated server needed, just SSH and python.
  • [+] easy variables - compared to Chef node attributes, which have like can be defined at a gazillion places and different syntaxes
  • [+] Batteries included: A lot of modules already included, so no need to browse Github for recipes
  • [-] Not as powerful as Ruby - Loops/conditionals are somewhat more limited to use

I won't look back to Chef. Chef always seemed to me a bit over-engineered and breaking a fly on the wheel for my usecases. Ansible looks very good, when you have to manage, say like <100 servers.


  1. Chef on Opscode http://www.opscode.com/chef/ 

  2. pludoni GmbH http://www.pludoni.de/team 

  3. Thread on hackernews about Ansible https://news.ycombinator.com/item?id=5244000 

  4. http://vagrantup.com 

  5. Besides static files, you can also create template-files, which are just files with special {{ var_name }} -syntax, to copy variables there. You can define your own variables in the vars: section of the playbook. Furthermore, there are some predefined variables, which you can display: 

    ansible all -m setup -i vagrant_ansible_inventory_default  --user=vagrant --private-key=~/.vagrant.d/insecure_private_key
    
  6. Handlers: are just commands that are executed after changes - like restarting a server after the config changed. They are defined in an independent section (handlers:) or into a different file, when using roles (roles/ROLENAME/handlers/main.yml) 


comments powered by Disqus