Ansible - Functional testing

You have done your automation in Ansible? Cool! But do you test, if the deployment really works? Do you check if your server is really listening to the correct port and delivers the correct content? This guide will dig into functional testing with Ansible...

Ansible - Functional testing

You have done your automation in Ansible? Cool! But do you test, if the deployment really works? Do you check if your server is really listening to the correct port and delivers the correct content? This guide will dig into functional testing with Ansible and demonstrate how you can check if everything went well.

Why should you test?

Ansible works the way, that you define a desired state, and it will ensure, that this state is present. So, why should you test this state again? There are a couple of scenarios, where it is helpful, that you can check the state in different ways.

A service may start like defined in Ansible, but crash some seconds later, unrecognized. This can happen with containers, but also with systemd services. Even if the service is running, there is no guarantee, that it is reachable from different endpoints, since firewalls, SELinux and permissions can interfere.

Lastly, some good functional testing can also be used for your development pipelines. If done right, you can test the status for different deployments and ensure that your playbook really works as intended.

How to functional test with Ansible?

Now that we know the "why", let's dig into the "how". Shall we? If you haven't done anything in Ansible, I strongly recommend to check out the "Ansible - Getting Started" guide and "Ansible - Playbooks" article, beforehand.

Simple example

Let's start with a very simple web server. The example code is really simple and can be used on any CentOS, Red Hat Enterprise Linux, Alma Linux or Fedora machine.

Hint
The guide is tested on Fedora 34 with ansible 2.9+.

You need to write a simple playbook (webserver.yml) like the one below.

---
- name: "Deploy a web server"
  hosts: "all"
  tasks:
  
    - name: "Manage httpd Packages"
      ansible.builtin.package:
        name: "httpd"
        state: "present"
      become: true
    
    - name: "Manage minimal Website"
      ansible.builtin.copy:
        content: "My cool WebPage"
        dest: "/var/www/html/index.html"
        owner: "root"
        group: "root"
        mode: 0644
      become: true

    - name: "Manage httpd Service"
      ansible.builtin.service:
        name: "httpd.service"
        state: "started"
        enabled: true
      become: true
      
    - name: "Manage http Firewall Policy"
      ansible.posix.firewalld:
        service: "http"
        state: "enabled"
        immediate: true
        permanent: true
      become: true
...
webserver.yml

Executing this playbook is also very easy. I am running this on localhost, but you can use it from a control host and manage one or more machines similarly.

# Run the playbook

$ ansible-playbook -K -k -i localhost, webserver.yml

The result will be, that the machine is reachable on port 80/TCP, hopefully. So, we can test this (manually for now).

# Test if everything is fine

$ curl localhost
My cool WebPage

Check Listening

The initial work is done with our little playbook example. But is there a way to test, if the server is really listening without doing it manually?

We can start with a simple check, that tests, if the desired port is reachable and working.

Let's extend our example playbook with a new task at the end.

---
- name: "Deploy a web server"
  hosts: "all"
  tasks:
  
    - name: "Manage httpd Packages"

...snip...

    - name: "Check Listening"
      ansible.builtin.wait_for:
        port: 80
        state: "started"
        timeout: 300
...
webserver.yml

This example will check if there is something active on port 80 for 300 seconds. The wait_for module can do even more, like waiting for a service or an existing path on the disk.

Check Working

The server seems fine and started. We tested the content is shipped with curl and the above example. But... there is an Ansible way, to "curl". Let's extend the above playbook with some more code, so we don't need to curl the website or test it in a browser.

---
- name: "Deploy a web server"
  hosts: "all"
  tasks:
  
    - name: "Manage httpd Packages"

...snip...

    - name: "Check Working"
      ansible.builtin.uri:
        url: "http://localhost"
...
webserver.yml

This will already do the trick, but it's only working for localhost. It would be better to check the actual host. This is also quite easy.

---
- name: "Deploy a web server"
  hosts: "all"
  tasks:
  
    - name: "Manage httpd Packages"

...snip...

    - name: "Check Working"
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}"
      delegate_to: "localhost"
...
webserver.yml

In the above example, the control node will execute the URI module and fetch the URL from the machine, defined in your inventory. This even works in our simple example, where the control node and managed node are the same thing.

Check Content

SELinux and permissions can be a problem, but also connections to a database or some issues in the deployed code can lead to unexpected behavior. In the last example, we just checked if the server delivers a website. But we also need to check, if the content is correct. This can be done with Ansible, too.

I assume, you expected this - let's put in another task and adjust our last task. Shall we?

---
- name: "Deploy a web server"
  hosts: "all"
  tasks:
  
    - name: "Manage httpd Packages"

...snip...

    - name: "Check Working"
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}"
        return_content: true
      delegate_to: "localhost"
      register: "r_working"

    - name: "Check Content"
      ansible.builtin.assert:
        that: "'My cool WebPage' is in r_working.content"
...
webserver.yml

The assert module can be used to test for all kind of things. It works mostly like the "when" statement, but the task will fail, if the statement is not met.


Update (Thanks to the feedback from a reader)

You can find some explanations about the "tests" you can use (basically Jinja Tests) in the Ansible documentation and the Jinja docs.

In general, you will these tests something like this: VARIABLE is JINJA_TEST. The VARIABLE can be anything you want, and the JINJA_TEST can be used as described in the documentations.


Security Checks

You can also use Ansible to check your server for common security issues. If you don't want to have MariaDB available in the public web, you can test this.

Sure, you can configure your server with Ansible to close a specific port, but what about "the other one" who has started a container with MySQL in it? Let's see how you can test this.

You may remember the wait_for module from one of the above sections. We can re-use it to check for absent/closed ports, too.

---
- name: "Deploy a web server"
  hosts: "all"
  tasks:
  
    - name: "Manage httpd Packages"

...snip...

    - name: "Check Mail closed"
      ansible.builtin.wait_for:
        port: 25
        state: "stopped"
        timeout: 300
...
webserver.yml

This can be very useful, if you want to reconfigure a server, that should not listen to one port any longer, but another one instead. You can also do security compliance tests with it to prove that telnet or postfix is not exposed/published on a system.

The full example

Everything from the above combined will look something like the below example.

---
- name: "Deploy a web server"
  hosts: "all"
  tasks:

    - name: "Manage httpd Packages"
      ansible.builtin.package:
        name: "httpd"
        state: "present"
      become: true

    - name: "Manage minimal Website"
      ansible.builtin.copy:
        content: "My cool WebPage"
        dest: "/var/www/html/index.html"
        owner: "root"
        group: "root"
        mode: 0644
      become: true

    - name: "Manage httpd Service"
      ansible.builtin.service:
        name: "httpd.service"
        state: "started"
        enabled: true
      become: true

    - name: "Manage http Firewall Policy"
      ansible.posix.firewalld:
        service: "http"
        state: "enabled"
        immediate: true
        permanent: true
      become: true

    - name: "Check Listening"
      ansible.builtin.wait_for:
        port: 80
        state: "started"
        timeout: 300

    - name: "Check Working"
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}"
        return_content: true
      delegate_to: "localhost"
      register: "r_working"

    - name: "Check Content"
      ansible.builtin.assert:
        that: "'My cool WebPage' is in r_working.content"

    - name: "Check Mail closed"
      ansible.builtin.wait_for:
        port: 25
        state: "stopped"
        timeout: 300
...
webserver.yml

You can extend this to your needs, put it into dedicated playbooks or roles, and re-use the content whenever you need it. But you can be sure, that you don't need to manually check again, after Ansible has deployed something. You can be sure, that it works.

More options

There are even more options to check your deployment for validity, test if a task was really done and is working as expected, or even check for security. The below list will give some ideas, where you can start digging.

There is even more that might be helpful, depending on your Ansible setup and the environment, you are managing.

You can find a couple of Links and videos about this topic in the web, too.

Ansible and using Automation to Assert IT Compliance
Learn how to use ansible for IT compliance use cases, including periodic remediation, CIS, STIG, PCI, and others. Learn about how to schedule ansible runs, use check mode (dry run) and how to integrate tests with ansible playbooks.
Testing Strategies — Ansible Documentation

Conclusion

As you can see, functional testing is possible with Ansible and also very helpful. I am a big fan of having a couple of tests ready, so I can check my deployments occasionally or whenever I change some code.