mtpereira

Sharing thoughts on systems automation and development.

About me

Ansible Tips: Testing roles using Vagrant and TravisCI

It’s very useful to keep your roles on TravisCI or other publicly available CI system: it enables you to verify that your commits and other contributions to your role don’t break it.

On the other hand, it’s a little tedious to be constantly waiting for the CI to finish and give you some results. Heck, you might even feel tempted to git push without actually checking if your changes are OK! ;)

For that reason, I find it really useful to keep a set of tools on each role’s repository to be able to check if I’m breaking anything whilst introducing new features or modifying existing ones.

I’ll be using the role mtpereira.ghost as an example.

Playbook for testing

A test.yml playbook is used for both TravisCI and Vagrant in order to run the role and testing the resulting host’s behaviour on both environments.

A first play is used for setting up our tests:

- name: tests - apply role
  hosts: all
  tags: ghost_tests_apply

  roles:
    - .

  post_tasks:
    - name: tests - sleep for Ghost startup
      pause:
        seconds: 15
        prompt: "Waiting for Ghost startup..."

    - name: tests - install curl
        apt:
          pkg: curl
          install_recommends: no
          state: present
        sudo: yes

Notice that in order to be able to execute the role from the current directory, it’s necessary to reference it by its path, .. This has several advantages over including all the role’s files (tasks, handlers, vars, etc) directly onto the playbook, which is something that I’ve done previously on several roles:

  • You’re actually running the role as a whole, which might prove very different than running its individual components. This is crucial when you’re role has dependencies on other roles, as they’ll only be executed when the role is invoked. In this example, the role depends on external roles and, as such, it’s necessary to guarantee that they’re executed properly and that they provide the required behaviour.
  • When you’ll add new files to the role, you won’t have to remember to add them to the test.yml playbook.
  • It’s cleaner and easier to read.

Also notice the post_tasks block after the role. These are for ensuring that we’re in conditions to actually run the tests and are very specific to this case.

The next play includes both the execution and validation steps of the tests:

- name: tests - run tests
  hosts: all
  tags: ghost_tests_run

  tasks:
    - name: tests - check if ghost is listening
      command: curl --silent {{ ghost_config_url }}
      changed_when: false
      ignore_errors: yes
      register: ghost_test_listening

    - name: tests - check why ghost is not listening
      command: node {{ ghost_install_dir }}/index.js
      args:
        chdir: "{{ ghost_install_dir }}"
        env:
          NODE_ENV: production
      changed_when: false
      ignore_errors: yes
      sudo_user: "{{ ghost_user_name }}"
      sudo: yes
      when: ghost_test_listening | failed

    - name: tests - assert that Ghost is listening
      assert:
        that: "{{ ghost_test_listening | success }}"

    - name: tests - check if Nginx is proxying Ghost
      command: curl --silent localhost:{{ ghost_nginx_port }}
      changed_when: false
      ignore_errors: yes
      register: ghost_test_nginx_proxy

    - name: tests - assert that Nginx is proxying Ghost
      assert:
        that: "{{ ghost_test_nginx_proxy | success }}"

    - name: tests - check if Ghost admin is protected from outside
      command: curl --silent --head localhost:{{ ghost_nginx_port }}/ghost/setup/
      changed_when: false
      ignore_errors: yes
      register: ghost_test_nginx_admin

    - name: tests - assert that Ghost admin is protected from outside
      assert:
        that: "'404 Not Found' in ghost_test_nginx_admin.stdout"

There are a few note-worthy details here:

  • ignore_errors: yes ensures that even if our exercise setup returns an erroneous exit code, we’ll still get to assert it, which effectively separates the execution and the validation on different tasks.
  • changed_when: false avoids that our tests change the idempotence tests’ results on TravisCI.
  • Testing using Ansible is not that pretty but is simple enough for testing role’s as a atomic component of larger playbooks (which should probably be tested using something like Serverspec.

Ansible Galaxy requirements file

If the role depends on other roles, it’s necessary to add a requirements.yml file for fetching said roles both locally and on TravisCI:

- src: nodesource.node
  version: master

- src: jdauphant.nginx
  version: v1.3.4

Vagrantfile

Usually I keep a Vagrantfile that looks something like this.

boxes = {
  "ghost-debian-wheezy" => {
                        :box => "opscode-debian-7.8",
                        :url => "https://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_debian-7.8_chef-provisionerless.box",
                        :ip  => '10.0.21.2',
                        :cpu => "100",
                        :ram => "128"
                      },
  "ghost-ubuntu-trusty" => {
                        :box => "opscode-ubuntu-14.04",
                        :url => "https://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_ubuntu-14.04_chef-provisionerless.box",
                        :ip  => '10.0.21.3',
                        :cpu => "100",
                        :ram => "128"
                      },
}

Vagrant.configure("2") do |config|
  boxes.each do |box_name, box|
    config.vm.define box_name do |machine|
      machine.vm.box = box[:box]
      machine.vm.box_url = box[:url]
      machine.vm.hostname = "%s" % box_name

      machine.vm.provider "virtualbox" do |v|
        v.customize ["modifyvm", :id, "--cpuexecutioncap", box[:cpu]]
        v.customize ["modifyvm", :id, "--memory",          box[:ram]]
      end

      machine.vm.network :private_network, ip: box[:ip]

      machine.vm.provision :shell do |shell|
        shell.inline = "sed -i -e 's/%sudo\tALL=NOPASSWD:ALL/%sudo\tALL=(ALL:ALL) ALL/' /etc/sudoers"
      end

      machine.vm.provision :ansible do |ansible|
        ansible.playbook = "test.yml"
        ansible.verbose = ENV['ANSIBLE_VERBOSE'] ||= "v"
        ansible.tags = ENV['ANSIBLE_TAGS'] ||= "all"
      end
    end
  end
end

A multihost Vagrantfile allows for testing against multiple operating systems, which might prove difficult to do on a public CI system. In this case, we have a Ubuntu and a Debian host. If we run vagrant up we’ll provision both VMs but we can also specify the a name for Vagrant so that it’ll only provision said host:

vagrant up ghost-ubuntu-trusty

The ANSIBLE_VERBOSE environment variable enables controlling Ansible’s verbosity whilst still using Vagrant for testing:

ANSIBLE_VERBOSE=vv vagrant provision

The same goes for ANSIBLE_TAGS, for controlling which tags to run. This is very useful for decreasing the write-test-debug cycle on specific Ansible tasks. Tagging your role properly is crucial for this, which we’ll explore on a future post.

TravisCI

The .travis.yml file that I use here is the same found on most roles: it tests the role and then tests it’s idempotence. It uses the same exact test.yml playbook used on Vagrant. The only note-worthy detail here is that we need to run ansible-galaxy on the before_script block:

install:
  - pip install ansible==1.9.1
  - "printf '[defaults]\\nroles_path = ../' > ansible.cfg"

before_script:
  - echo localhost > inventory
  - ansible-galaxy install --role-file requirements.yml

Conclusion

With these tools, I find that the role’s development cycle usually goes something like this:

  1. Provision hosts and run test playbook with vagrant up
  2. Fix bugs
  3. Re-run the affected tasks using ANSIBLE_TAGS=tag_name vagrant provision
  4. Repeat 2. and 3. as needed
  5. Reprovision host for full role test: vagrant destroy && vagrant up
  6. git push and check CI output for errors
  7. Go to 1. if necessary
  8. vagrant destroy for clean up

If you have any question or if you’ve found some mistake on this post, please drop me a line using any of my contacts.

Resources

  1. Testing Ansible Roles with Travis CI on GitHub”