Engineering Blog
BackExecutable Ansible Playbooks
Why we use chmod +x
on our playbooks and why that is a good idea.
We use Ansible to provision, configure, and orchestrate our infrastructure. The code has grown over the years, and systems have sprung up that we need to interact with when a playbook is running. We manage our inventory in Netbox, we record playbook runs using ARA, and store secrets in Vault/OpenBao.
As a result, our playbooks require more and more knowledge about our environment, and the chance of "holding them wrong" increases.
How it Started
Like most Ansible users, we initially called all playbooks like this:
$ ansible-playbook playbooks/state/all.yml -i inventory/prod-rma1 -l www --diff
This works great, but it is more verbose than necessary. Some in our team, including - if not limited to - me, prefer short commands over long ones. Especially if they need to be used frequently.
So we took our existing playbooks like this one:
- name: Base network and boot setup
hosts: '{{ playbook_hosts | default("ansible-managed") }}'
And added a shebang:
#!/usr/bin/env ansible-playbook
- name: Base network and boot setup
hosts: '{{ playbook_hosts | default("ansible-managed") }}'
Then we made the file executable:
$ chmod +x playbooks/state/all.yml
And presto, we could drop the command name from our CLI, calling the same playbook as follows:
$ playbooks/state/all.yml -i inventory/prod-rma1 -l www
How it is Going
When integrating ARA logging, we discovered that operators would always have to use --diff / -D
, for differences to actually be logged.
We figured that typing --diff
is not something we wanted to add to our documentation. We wanted to enforce it. We also had some configuration we needed to apply, depending on the inventory selection.
Maybe there is another way, but we figured: Since we already use our playbooks somewhat like CLIs, why not wrap ansible-playbook
and go all-in?
So that's what we did. We wrote our own Python CLI called ap
, that would inspect the arguments destined for ansible-playbook
, and introduce some of its own arguments for a better user experience.
The script uses Typer and looks roughly as follows (this is a minimized version to illustrate the concept):
import os
import subprocess
import sys
from typer import Context
from typer import Option
from typer import Typer
from typing import Annotated
cli = Typer(add_completion=False, add_help_option=False, no_args_is_help=True)
@cli.command(name='ap', context_settings={
"allow_extra_args": True,
"ignore_unknown_options": True
}, add_help_option=False, no_args_is_help=True)
def main(
ctx: Context,
inventory: Annotated[list[str], Option("--inventory", "-i", help=(
"Inventories to use, each either a site or a full inventory path"
))] = [],
) -> None:
args = ['ansible-playbook', *(a for a in ctx.args)]
# Configure inventories
for i in inventory:
i = i.removeprefix('inventory/')
args.append('-i')
args.append(f'inventory/{i}')
# Always use diff
if '-D' not in args and '--diff' not in args:
args.append('--diff')
# Configure systems
os.environ.update(ara_env(args))
os.environ.update(netbox_env(args))
os.environ.update(secrets_env(args))
# Execute
result = subprocess.run(args, env=os.environ)
sys.exit(result.returncode)
if __name__ == '__main__':
cli()
Aside from enforcing arguments, this also gave us the flexibility to shorten our inventory calls a little, as the inventory
prefix is really always the same. So while we started with this:
$ ansible-playbook playbooks/state/all.yml -i inventory/prod-rma1 -l www --diff
We can now call this, which is equivalent:
$ playbooks/state/all.yml -i prod-rma1 -l www
And now we can use all these saved keystrokes on useful things, like our engineering blog!
If you have comments or corrections to share, you can reach our engineers at engineering-blog@cloudscale.ch.