Introducing AI agent: Get information about your infrastructure faster. Learn more >

Getting started with Ansible playbooks in CFEngine

Posted by Lars Erik Wik
August 5, 2026

Recently, I’ve been playing around with the ansible promise type by Fabio Tranchitella. It’s amazing how easy it is to leverage the benefits of agents with all the existing automation from the ansible community. Hence, I wanted to share it in a blog post, with an emphasis on easy.

Let’s start off by creating a new cfbs project in a cfengine-ansible directory. Make sure to answer yes on the prompt to build on top of the default policy set. You may answer the remaining prompts as you see fit.

command
cfbs init
output
Please enter the name of this CFEngine Build project [Example project]: cfengine-ansible
Please enter the description of this CFEngine Build project [Example description]: Example of running Ansible playbooks with CFEngine
Do you want cfbs to initialize a git repository and make commits to it? [YES/y/no/n] no 
Initialized an empty project called 'cfengine-ansible' in 'cfbs.json'
Do you wish to build on top of the default policy set, masterfiles? (Recommended) [YES/y/no/n]: yes
Added module: masterfiles

Since ansible is not a built-in promise type in CFEngine, we’ll need to add it to our project from CFEngine Build.

command
cfbs add promise-type-ansible
output
Added module: library-for-promise-types-in-python (Dependency of promise-type-ansible)
Added module: promise-type-ansible

The addition of the custom promise type doesn’t pull in Ansible on its own. Hence, we’ll need make sure it’s installed on all the hosts. This can easily be achieved with a simple packages promise. Create the file ansible/ansible.cf within the cfbs project and add the following bundle.

bundle agent ansible_install
{
  packages:
    "ansible";
}

Next we can create our first playbook in ansible/playbook.yaml that simply touches a file.

- name: My first play
  hosts: localhost
  tasks:
   - name: Making changes
     ansible.builtin.file:
       path: /tmp/cfengine-ansible-probe
       state: touch

Unfortunately, or rather thankfully, the playbook won’t run on its own. We’ll have to explicitly tell CFEngine to run it using the ansible promise type (that we added earlier).

bundle agent ansible_playbook
{
  ansible:
    "$(this.promise_dirname)/playbook.yaml";
}

For the sake of convenience, let’s create one more bundle as a single entry point for calling the other bundles.

bundle agent ansible_main
{
  methods:
    "ansible_install";
    "ansible_playbook";
}

If we now add our ansible directory to our cfbs project, we’ll ensure that all of its contents will be copied into the build. Furthermore, cfbs will automatically detect the policy file and the bundles within it. When prompted, select ansible_main as the bundle to evaluate.

command
cfbs add ./ansible/
output
Which bundle should be evaluated (added to bundle sequence)?
 1. ./ansible/ansible.cf:ansible_install (default)
 2. ./ansible/ansible.cf:ansible_playbook
 3. ./ansible/ansible.cf:ansible_main
 4. (None)
 [1/2/3/4]: 3
Added module: ./ansible/

Now let’s build and hope for the happy chipmunk.

command
cfbs build
output
 ---snip---
Build complete, ready to deploy 🐿
 -> Directory: out/masterfiles
 -> Tarball:   out/masterfiles.tgz

To install on this machine: sudo cfbs install
To deploy on remote hub(s): cf-remote deploy

After installing / deploying the project according to the hints provided in the build output, you should be able to run the agent and see that the task successfully changed.

command
sudo cf-agent --no-lock --inform
output
[WARNING]: No inventory was parsed, only implicit localhost is available
    info: Task 'Making changes' successfully changed

Furthermore, we can stat the file to make sure we’ve been talking to our beloved cf-agent and not a hallucinating AI agent.

command
stat /tmp/cfengine-ansible-probe
output
  File: /tmp/cfengine-ansible-probe
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: 202,1	Inode: 2955        Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2026-07-31 14:14:05.346516492 +0000
Modify: 2026-07-31 14:14:05.346516492 +0000
Change: 2026-07-31 14:14:05.346516492 +0000
 Birth: 2026-07-31 12:32:03.662445650 +0000

Indeed, everything works as expected. However, there was that annoying warning from Ansible complaining about a missing inventory file. Let’s get rid of that by providing an inventory file using the following bundle to render it.

bundle agent ansible_inventory
{
  files:
    "$(sys.workdir)/ansible/inventory.ini"
      content => "[local]
localhost ansible_connection=local";
}

Next, we need to tell the ansible promise type where the inventory file is. Update the ansible_playbook bundle with the inventory attribute.

bundle agent ansible_playbook
{
  ansible:
    "$(this.promise_dirname)/playbook.yaml"
      inventory => "$(sys.workdir)/ansible/inventory.ini";
}

Last, but not least, add the new inventory bundle to our main bundle.

bundle agent ansible_main
{
  methods:
    "ansible_install";
    "ansible_inventory";
    "ansible_playbook";
}

If we build and deploy again, we can observe that the warning is gone on the next agent run.

command
sudo cf-agent -KI
output
    info: Task 'Making changes' successfully changed

And that concludes this blog post. However, if you need something more production ready, you should probably go ahead with a similar approach to what Craig Comstock did in his blog post on ansible.

If you have any questions about the ansible promise type, let us know at GitHub Discussions. We are eager to hear from you.