Test driven Development with Ansible

I develop ansible code for now around two years. Back then I quickly thought about how I could test my Ansible code properly. After a few weeks of searching I came up with molecule. I hope this post helps you to improve your automation processes, speeds up your ansible role development and increases their reliability.

Overview

Drivers are the environment in which I choose to run my playbooks. So it could be (as of now 2019–04–08) one of:

  • Vagrant
  • Docker
  • Delegated
  • EC2
  • GCE
  • Linode
  • LXC
  • LXD
  • Openstack
  1. SyntaxCreate
  2. Prepare Tests
  3. Converge Tests
  4. Idempotence Tests
  5. Side effects Tests
  6. Verify Tests
  7. Cleanup

Working with Molecule

In my examples below I will use Docker as my driver only. It was my choice because of the ligthweight. whenever I develop a new role, I can just run it locally and exec into the container if somethings does not go well. I can quickly test multiple operating systems like Debian, CentOS, RedHat or Ubuntu.
I also will use inspec as my verifier and of course yamlint as my linter of choice.

Preparation

1. of we will create a virtualenv

danny@mylaptop:~$ virtualenv /tmp/.env
Running virtualenv with interpreter /home/linuxbrew/.linuxbrew/bin/python2
New python executable in /tmp/.env/bin/python2
Also creating executable in /tmp/.env/bin/python
Installing setuptools, pkg_resources, pip, wheel...done.
danny@mylaptop:~$ source /tmp/.env/bin/active
(.env) danny@mylaptop:~$
(.env) danny@mylaptop:~$ pip install molecule==2.20
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7.
Collecting molecule==2.20
Downloading
[..omitted..]Successfully installed ansible-lint-4.1.0 entrypoints-0.3 flake8-3.7.7 functools32-3.2.3.post2 idna-2.7 molecule-2.20.0 pbr-5.1.1 pycodestyle-2.5.0 pyflakes-2.1.1 testinfra-1.19.0 typing-3.6.6
(.env) danny@mylaptop:~$ pip list |grep -e molecule -e ansible
ansible 2.7.9
ansible-lint 4.1.0
molecule 2.20.0
testinfra 1.19.0
(.env) danny@mylaptop:~$ molecule init role --help
Usage: molecule init role [OPTIONS]
Initialize a new role for use with Molecule.Options:
--dependency-name [galaxy] Name of dependency to initialize. (galaxy)
-d, --driver-name [azure|delegated|docker|ec2|gce|linode|lxc|lxd|openstack|vagrant]
Name of driver to initialize. (docker)
--lint-name [yamllint] Name of lint to initialize. (yamllint)
--provisioner-name [ansible] Name of provisioner to initialize. (ansible)
-r, --role-name TEXT Name of the role to create. [required]
--verifier-name [goss|inspec|testinfra]
Name of verifier to initialize. (testinfra)
--help Show this message and exit.
(.env) danny@mylaptop:~$ molecule init role --role-name role-name --verifier-name inspec --driver-name docker --lint-name yamllint
--> Initializing new role role-name...
Initialized role in /tmp/role-name successfully.
(.env) danny@mylaptop:~$ tree role-name/
role-name/
├── defaults
│ └── main.yml
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── molecule
│ └── default
│ ├── Dockerfile.j2
│ ├── INSTALL.rst
│ ├── molecule.yml
│ ├── playbook.yml
│ ├── tests
│ │ └── test_default.rb
│ └── verify.yml
├── README.md
├── tasks
│ └── main.yml
└── vars
└── main.yml
8 directories, 12 files

Test drive approach

Now we are writing our tests first. So think about what you are trying to achive using this role.

  1. ensure that the service is enabled (starts on boot),running and the service is present
  2. Lastely we have to check for certain configurations also regarding to some CVs
describe package('openssh-server') do
it { should be_installed }
its('version') { should cmp >= '1:7.4p1-10+deb9u4' }
end
describe service('ssh.service') do
it { should be_enabled }
it { should be_installed }
it { should be_running }
end
describe sshd_config do
its('content') { should match(%r{^Port}) }
its('Port') { should_not cmp ['22','1022','10022'] }
its('Protocol') { should cmp ['2', '3']}
its('content') { should match(%r{^UseDNS}) }
its('UseDNS') { should cmp('no') }
its('LogLevel') { should cmp('VERBOSE') }
end

Writing the ansible-role

Now that we have our tests in place, we can start to create our ansible-role itself.

- name: SSHD | Check if everything is installed
include_tasks: Debian_install.yml
when: ansible_os_family == "Debian" or ansible_os_family == "Ubuntu"
- name: SSHD | Configure and run SSH-Server
import_tasks: sshd.yml
---
- name: SSHD | Install openssh
apt:
name:
- openssh-server
state: present
- name: SSHD | Start and enable
service:
name: sshd
state: started
enabled: yes
---
- name: SSHD | Configure sshd
template:
src: sshd/sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0600'
validate: /usr/sbin/sshd -t -f %s
notify: SSHD | restart - config changed
- name: SSHD | restart - config changed
service:
name: sshd.service
state: restarted
galaxy_info:
author: Danny
description: role-name
# If the issue tracker for your role is not on github, uncomment the
# next line and provide a value
# issue_tracker_url: http://example.com/issue/tracker
# Some suggested licenses:
# - BSD (default)
# - MIT
# - GPLv2
# - GPLv3
# - Apache
# - CC-BY
license: GPLv2
min_ansible_version: 1.2# Optionally specify the branch Galaxy will use when accessing the GitHub
# repo for this role. During role install, if no tags are available,
# Galaxy will use this branch. During import Galaxy will access files on
# this branch. If travis integration is cofigured, only notification for this
# branch will be accepted. Otherwise, in all cases, the repo's default branch
# (usually master) will be used.
#github_branch:
#
# Below are all platforms currently available. Just uncomment
# the ones that apply to your role. If you don't see your
# platform on this list, let us know and we'll get it added!
#
platforms:
[...]
extends: defaultignore: |
molecule/
meta/
rules:
braces:
max-spaces-inside: 1
level: error
brackets:
max-spaces-inside: 1
level: error
line-length: disable
truthy: disable
[...]
privileged: True
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
capabilities:
- SYS_ADMIN
- NET_ADMIN
[...]
[...]
lint:
name: rubocop
enabled: false
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
enabled: True
options:
config-file: .yamllint
format: parsable
platforms:
- name: centos7-${BUILD_ID}
image: molecule/centos7:latest
registry:
url: <myRegistry>
command: /lib/systemd/systemd
privileged: True
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
capabilities:
- SYS_ADMIN
- NET_ADMIN
- name: debian9-${BUILD_ID}
image: molecule/debian9:latest
registry:
url: <myRegistry>
command: /lib/systemd/systemd
privileged: True
security_opts:
- seccomp=unconfined
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /tmp
- /run
capabilities:
- SYS_ADMIN
- NET_ADMIN
provisioner:
name: ansible
lint:
name: ansible-lint
enabled: True
options:
exclude:
- molecule/*
- .molecule/*
- meta/*
scenario:
name: default
verifier:
name: inspec
lint:
name: rubocop
enabled: false
$ molecule test
--> Validating schema [...]/role-name/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix

└── default
├── lint
├── cleanup
├── destroy
├── dependency
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
├── cleanup
└── destroy

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in [...]/role-name/...
[...]/role-name/molecule/default/molecule.yml:48:7: [error] wrong indentation: expected 8 but found 6 (indentation)
[...]/role-name/tasks/sshd.yml:1:1: [warning] missing document start "---" (document-start)
[...]/role-name/tasks/main.yml:6:1: [error] trailing spaces (trailing-spaces)
An error occurred during the test sequence action: 'lint'. Cleaning up.
--> Scenario: 'default'
--> Action: 'destroy'

Sum-up

  • Overview what Molecule does and have
  • How you can quickly develop on your local machine
  • How to write basic lines of inspec for simple testings
  • How the ansible/molecule flow looks like

Closing

This is how you develop your ansible code by using Molecule, Docker and Inspec.

Links:

Working as a IT-Operations engineer at NeXenio, a spin-off by Hasso-Plattner-Institute for products around a digitial workspace.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store