Skip to main content

Test Ansible using policy as code with Conftest

Conftest evaluates Open Policy Agent (OPA) policies against structured configuration files. Learn how to apply it to your Ansible Playbooks.
Test tubes

Photo by Kindel Media from Pexels

Policy as code is one of the most popular trends in the cloud-native space. Many discussions about policy as code focus on tools like Kubernetes, web services, and identity and access management (IAM). However, policy as code practices are equally effective for more traditional systems administration tasks.

[ Do you know the difference between Red Hat OpenShift and Kubernetes? ]

This article will show you how to use Conftest to enforce policy-as-code decisions against Ansible Playbooks.

About OPA, Rego, and Conftest

Open Policy Agent (OPA) is a Cloud Native Computing Foundation graduated project that allows you to define and evaluate policy as code. OPA has several deployment models, including an HTTP-based server and a library. While it is commonly used in Kubernetes environments, OPA also provides a command-line interface for directly working with policy.

OPA operates on JSON input. JSON works well for web services, such as Kubernetes admission webhooks. However, policy-as-code patterns are useful for much more than just web services. Many applications rely on various types of structured configurations, such as YAML, TOML, INI, and other file formats. Conftest extends OPA to support different kinds of structured text files.

OPA policy is defined using the Rego policy language. Rego makes it easy to query complex, hierarchical structured data and provides many features to enable reusable and composable policies.

This article assumes a basic understanding of Rego. Review the basic Rego introduction if you have never worked with Rego policies before. The examples in this article are very simple and provide a basic understanding of Rego.

Install Conftest

Conftest is available as a binary utility you can download from its releases page. Packages are also available in RPM and DEB formats. Install Conftest by downloading the binary or with a call to your package manager:

# dnf install -y
Last metadata expiration check: 0:00:46 ago on Wed 18 Jan 2023 11:03:05 AM EST.
conftest_0.38.0_linux_amd64.rpm                                                                                                                      5.8 MB/s |  13 MB     00:02    
Dependencies resolved.
 Package                                    Architecture                             Version                                    Repository                                      Size
 conftest                                   x86_64                                   0.38.0-1                                   @commandline                                    13 M

Transaction Summary
Install  1 Package

Total size: 13 M
Installed size: 36 M
Downloading Packages:
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                                                                                                                             1/1 
  Installing       : conftest-0.38.0-1.x86_64                                                                                                                                    1/1 
  Verifying        : conftest-0.38.0-1.x86_64                                                                                                                                    1/1 



[root@fedora-1 ~]# conftest -v
Conftest: 0.38.0
OPA: 0.48.0

[ Want to test your sysadmin skills? Take a skills assessment today. ]

Policy scenario

For this article, I will develop an installation policy for the MariaDB-server package. I have added the MariaDB repositories for MariaDB versions 10.8 and 10.9 to a remote server to enable the installation of several different MariaDB versions:

# dnf list --showduplicates Mariadb-server
Last metadata expiration check: 0:12:39 ago on Wed 18 Jan 2023 11:13:37 AM EST.
Available Packages
MariaDB-server.x86_64                                                                  10.8.4-1.fc36                                                                     mariadb-10.8
MariaDB-server.x86_64                                                                  10.8.5-1.fc36                                                                     mariadb-10.8
MariaDB-server.x86_64                                                                  10.8.6-1.fc36                                                                     mariadb-10.8
MariaDB-server.x86_64                                                                  10.9.2-1.fc36                                                                     mariadb-10.9
MariaDB-server.x86_64                                                                  10.9.3-1.fc36                                                                     mariadb-10.9
MariaDB-server.x86_64                                                                  10.9.4-1.fc36                                                                     mariadb-10.9
mariadb-server.x86_64                                                                  3:10.5.13-1.fc36                                                                  fedora      
mariadb-server.x86_64                                                                  3:10.5.18-1.fc36                                                                  updates     

While the repositories expose several versions for installation, I would like to limit the installable versions to the latest version of each minor release (10.8.6 and 10.9.4). I want to ensure that an Ansible Playbook to install MariaDB-server will install an approved version.

This type of policy can be expressed as code using Rego and evaluated using conftest. I find it helpful to express the policy in plain language before writing any code:

If a play installs the MariaDB-server package, it should be an approved version.

With this plain-text definition in place, it is possible to translate it into code.

Understand policy input

An Ansible Playbook is written in YAML, but OPA operates on JSON input. Conftest takes existing structured configuration files and converts them to JSON. To evaluate a structured configuration file, you must understand how the configuration file is presented to the policy.

The playbook below installs a specific version of the MariaDB-server package. The first step in understanding how conftest parses this playbook is understanding how the policy code will be structured:

- name: Install MariaDB
  hosts: dbservers
    - name: Install MariaDB-server
        name: MariaDB-server-10.8.4-1.fc36
        state: present

The parse subcommand processes a file and outputs the resulting JSON input to an OPA policy. The Ansible Playbook to install MariaDB results in the following JSON:

$ conftest parse playbook.yaml 
    "hosts": "dbservers",
    "name": "Install MariaDB",
    "tasks": [
        "ansible.builtin.package": {
          "name": "MariaDB-server-10.8.4-1.fc36",
          "state": "present"
        "name": "Install MariaDB-server"

This JSON is presented to a Rego policy as part of the input document. Rego queries this input document to make policy decisions.

[ Learn how to manage your Linux environment for success. ]

Write the policy

Once you understand the desired policy outcome and the input document format, you can begin writing the policy. First, create a new policy/ directory and place a main.rego file in it. Conftest searches for Rego files in the policy/ subdirectory by default. You can change this behavior with the -p flag to specify different directories.

$ mkdir policy

$ touch policy/main.rego

$ tree
├── inventory.yaml
├── playbook.yaml
└── policy
    └── main.rego

The main.rego file contains the policy. This file can have any name, and you can define policies across multiple files. The first statements in the main.rego file contain the necessary boilerplate code. A Rego policy must have a package statement. Conftest will process policies only in the main package by default, but you can modify this behavior by passing the --all-namespaces flag. Importing certain future keywords make policies easier to read:

package main

import future.keywords.contains
import future.keywords.if

Next, define a list of allowed MariaDB versions. This simple example stores this information alongside the policy code. However, it often makes sense to store data used in decision making, such as a list of allowed application versions, outside the policy code itself. Conftest can consume external data using the -d flag, but this option is outside the scope of this example.

allowed_mariadb_versions = [

Define a single rule called deny_unapproved_version. The name of a rule is significant to conftest:

  • A deny rule will trigger a policy evaluation failure and a non-zero exit status.
  • A warn rule will trigger a warning, and the policy evaluation will still exit successfully.
  • A violation rule behaves like a deny rule but allows you to specify additional fields in the returned message.
deny_unapproved_version contains msg if {
  some task in input.tasks
  package_name := task["ansible.builtin.package"].name
  regex.match("MariaDB-server.*", package_name)
  version := split(package_name, "-")[2]
  not version in allowed_mariadb_versions

  msg := sprintf("Version %v of Mariadb-Server is not allowed", [version])

Rego is a query language, and this rule searches across the JSON input to find conditions that make the rule evaluate to true. Each statement in the rule body must evaluate to true. If all its statements evaluate to true, this rule generates a virtual document containing a message to the user.

The rule searches across all tasks in the playbook to find any using the ansible.builtin.package module. It checks whether the package name matches the MariaDB-server regular expression. If it does, the package name is split on the - character to obtain the version number. If the version is not in the list of allowed versions, then the not version in allowed_mariadb_versions statement evaluates to true, and a message is returned to the user.

If any of these conditions fails, the rule evaluation halts, the policy evaluation passes, and no message is returned to the user.

Here are the full contents of the main.rego file:

package main

import future.keywords.contains
import future.keywords.if

allowed_mariadb_versions = [

deny_unapproved_version contains msg if {
  some task in input.tasks
  package_name := task["ansible.builtin.package"].name
  regex.match("MariaDB-server.*", package_name)

  version := split(package_name, "-")[2]
  not version in allowed_mariadb_versions

  msg := sprintf("Version %v of Mariadb-server is not allowed", [version])

Run the policy

The test subcommand executes conftest against an input file, such as an Ansible Playbook. Conftest will parse the input file, evaluate the policies in the policy/ subdirectory, and return any messages to the user. The current playbook fails the policy evaluation because MariaDB-server version 10.8.4 is not included in the list of allowed versions. The command also exits with a non-zero exit status, which is useful in pipelines to signal failure.

$ conftest test playbook.yaml 
FAIL - playbook.yaml - main - Version 10.8.4 of Mariadb-server is not allowed

1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions

$ echo $?

Adjusting the playbook to use an approved version of MariaDB-server, such as version 10.8.6, allows the playbook to pass policy evaluation, and the conftest command exits with a zero exit status:

# Replace the MariaDB-server version 10.8.4 with 10.8.6 (an approved version)
$ sed -ie 's/MariaDB-server-10.8.4-1.fc36/MariaDB-server-10.8.6-1.fc36/g' playbook.yaml

$ conftest test playbook.yaml 

1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions

$ echo $?

[ Get started with IT automation with the Ansible Automation Platform beginner's guide. ]

Wrap up

This article introduced conftest, a tool that evaluates OPA policies against structured configuration files. You saw a simple example of how a policy can be expressed as code and evaluated against an Ansible Playbook. While this is a basic example, conftest provides many features that make it an ideal tool for extending policy as code practices to other types of system configuration. I encourage you to visit the Conftest website to learn more and develop your own ideas for leveraging policy as code in your automation workflows.

I also encourage you to build on this example by extending the policy to match tasks where no version is specified for MariaDB-server or to match tasks that directly use a specific package manager's module, such as the ansible.builtin.dnf module.

Topics:   Ansible   Security   Testing  
Author’s photo

Anthony Critelli

Anthony Critelli is a Linux systems engineer with interests in automation, containerization, tracing, and performance. He started his professional career as a network engineer and eventually made the switch to the Linux systems side of IT. He holds a B.S. and an M.S. More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.