How I use Ansible


If there is some work-flow, configuration or tweak to a system to be made you best believe it’s going into a playbook because otherwise I will forget. Becoming a yaml-farmer is not what I envisioned for myself but here we are. Almost every time I use Ansible to implement something I learn some new trick, which can be annoying or cool depending on my mood. Tools from Redhat tend to be pretty bleeding-edge so this is no exception. When I first started writing simple playbooks ansible-galaxy had just become a thing, but now there is so much more, sheesh.

Here’s my setup, which you can easily repurpose for your own projects.

ansible-navigator + ansible-builder

It feels a little silly that now there are even more cli tools to juggle, instead of the usual ansible-playbook task.yml. Now we have… ’execution environments’? I LOVE learning new jargon, I LOVE having to memorize words for things that already exist.

Anyway, an execution environment is just a container image which comes with all the ansible-core and python dependencies. Much in the same way that it doesn’t really make sense to run builds or batch jobs on your own development laptop, ansible-builder allows you to create an execution environment for a control node on a remote server with all the ansible-galaxy collections and python dependencies setup and ready to go. Share with other yaml-farmers on your team as you please. Python dependencies for me have always been pure pain to configure so I am willing to take the assist.

ansible-navigator is the UI that wraps everything together and becomes the new entrypoint for executing commands against the execution environment. For example:

ansible-vault encrypt group_vars/all/vault_passwords.yml

becomes

ansible-navigator exec -- ansible-vault encrypt group_vars/all/vault_passwords.yml

or alternatively

ansible-navigator exec "ansible-vault encrypt group_vars/all/vault_passwords.yml"

and

ansible-playbook website-deploy.yml

becomes

ansible-navigator run website-deploy.yml

How delightfully madenning it is having to memorize even more wrappers and abstractions. Furthermore, one annoying detail I discovered is that you have to escape whitespace if you’re running ad-hoc commands.

So

ansible-navigator exec -- ansible gwt -m file -a "path=/path/to/some/file.txt state=absent"

will error out but

ansible-navigator exec -- ansible gwt -m file -a "path=/path/to/some/file.txt\ state=absent"

succeeds. Most uncool.

Configuring the configurators

You can get a good overview of what exactly navigator is capable of doing by checking out its interactive mode ansible-navigator --mode interactive via the :settings.

   Name                                                                Default       Source                                          Current
 0│Ansible runner artifact dir                                         False         Settings file                                   /run/user/1000
 1│Ansible runner rotate artifacts count                               False         Settings file                                   10
 2│Ansible runner timeout                                              True          Not set                                         Not set
 3│Ansible runner write job events                                     True          Defaults                                        False
 4│App                                                                 False         Command line                                    collections
 5│Cmdline                                                             True          Not set                                         Not set
 6│Collection doc cache path                                           True          Defaults                                        /home/programmist/.cache/ansible-navigator/collection_doc_cache.db
 7│Config                                                              True          Not set                                         Not set
 8│Container engine                                                    False         Settings file                                   podman
 9│Container options                                                   False         Settings file                                   ['--user=0', '--user=root']
10│Current settings file                                               False         Search path                                     /home/programmist/.ansible-navigator.yml
11│Display color                                                       True          Defaults                                        True
12│Editor command                                                      True          Defaults                                        nvim {filename}
13│Editor console                                                      True          Defaults                                        True
14│Enable prompts                                                      True          Defaults                                        False
15│Exec command                                                        True          Defaults                                        /bin/bash
16│Exec shell                                                          True          Defaults                                        True
17│Execution environment                                               True          Settings file                                   True
18│Execution environment image                                         False         Settings file                                   custom-ee:latest
19│Execution environment volume mounts                                 True          Not set                                         Not set
20│Format                                                              True          Settings file                                   yaml
21│Help builder                                                        True          Defaults                                        False
22│Help config                                                         True          Defaults                                        False
23│Help doc                                                            True          Defaults                                        False
24│Help inventory                                                      True          Defaults                                        False
25│Help playbook                                                       True          Defaults                                        False
26│Images details                                                      True          Defaults                                        ['everything']
27│Inventory                                                           False         Ansible configuration file                      ['/home/programmist/projects/mine/generic-wizard-tower/hosts.yml']
28│Inventory column                                                    True          Not set                                         Not set
29│Lint config                                                         True          Not set                                         Not set
30│Lintables                                                           True          Not set                                         Not set
31│Log append                                                          True          Defaults                                        True
32│Log file                                                            False         Settings file                                   /home/programmist/projects/mine/ansible-navigator.log
33│Log level                                                           False         Settings file                                   critical
34│Mode                                                                True          Command line                                    interactive
35│Osc4                                                                True          Defaults                                        True
36│Pass environment variable                                           False         Settings file                                   ['ANSIBLE_VAULT_PASSWORD_FILE']
37│Playbook                                                            True          Not set                                         Not set
38│Playbook artifact enable                                            True          Settings file                                   True
39│Playbook artifact replay                                            False         Settings file                                   /tmp/test_artifact.json
40│Playbook artifact save as                                           False         Settings file                                   /tmp/test_artifact.json
41│Plugin name                                                         True          Not set                                         Not set
42│Plugin type                                                         True          Defaults                                        module
43│Pull arguments                                                      True          Not set                                         Not set
44│Pull policy                                                         False         Settings file                                   missing
45│Set environment variable                                            False         Settings file                                   {'ANSIBLE_STDOUT_CALLBACK': 'yaml'}
46│Settings effective                                                  True          Defaults                                        False
47│Settings sample                                                     True          Defaults                                        False
48│Settings schema                                                     True          Defaults                                        json
49│Settings sources                                                    True          Defaults                                        False
50│Time zone                                                           True          Defaults                                        UTC
51│Workdir                                                             True          Defaults                                        /home/programmist/projects/mine/generic-wizard-tower

As you can see its a mix of config values from differents sources; environment variables, defaults and files. Note the execution environment image, which is created with ansible-builder build -t custom-ee:latest --prune-images -v3. The config for the image references the requirements.yml and requirements.txt for any collections from galaxy you might want to use in your playbooks, such as Podman modules:

~/projects/mine/gwt/execution-environment.yml

version: 3

images:
  base_image:
    name: ghcr.io/ansible/community-ansible-dev-tools:latest

dependencies:
  ansible_core:
    package_pip: ansible-core
  ansible_runner:
    package_pip: ansible-runner
  galaxy: requirements.yml
  python: requirements.txt
  system:
    - openssh-clients
    - sshpass

options:
  package_manager_path: /usr/bin/dnf5

Of course, its own config is also found at ~/.ansible-navigator.yml. Other important things you’ll probably want to set are your vault password file, log levels and log file location. I like to see prettier yaml so I use the stdout callback function.

---
ansible-navigator:
  ansible-runner:
    artifact-dir: /run/user/1000
    rotate-artifacts-count: 10
  execution-environment:
    container-engine: podman
    container-options: ["--user=0"]
    enabled: true
    environment-variables:
      pass:
        - ANSIBLE_VAULT_PASSWORD_FILE
      set:
        ANSIBLE_STDOUT_CALLBACK: yaml
    image: custom-ee:latest
    pull:
      policy: missing
  logging:
    file: /home/programmist/projects/mine/ansible-navigator.log
    level: critical
  playbook-artifact:
    enable: True
    replay: /tmp/test_artifact.json
    save-as: /tmp/test_artifact.json
  mode: stdout
  format: yaml

Project directory structure

There are a multitude of ways to structure an Ansible project but here is what works for me. My extremely generalized, happy-path organization is as follows.

.
├── files
│   ├── grafana
│   │   ├── dashboards
│   │   │   ├── dashboard.yml
│   │   │   ├── victoriametrics.json
│   │   │   ├── vmagent.json
│   │   │   └── vmalert.json
│   │   └── prometheus-datasource
│   │       └── single.yml
│   ├── hugo
│   │   ├── rpavlov.com
│   │   └── othersites.com
│   ├── playlists
│   ├── samba
│   ├── vm-agent
│   ├── vm-alert
│   │   └── rules
│   ├── vm-alertmanager
│   └── zsh
├── group_vars
│   ├── all
│   │   ├── common.yml
│   │   └── vault_passwords.yml
│   └── bunker
│       ├── authelia.yml
│       └── htpasswd.yml
├── host_vars
│   ├── arcane_sanctum.yml
│   ├── gwt.yml
│   ├── secret_hetzner_tunnel.yml
│   └── secret_oracle_tunnel.yml
├── roles
│   ├── dns
│   │   └── tasks
│   ├── podman_container
│   │   ├── defaults
│   │   └── tasks
│   ├── podman_pod
│   │   ├── defaults
│   │   └── tasks
│   ├── redis
│   │   ├── defaults
│   │   └── tasks
│   ├── ssh
│   │   ├── handlers
│   │   └── tasks
│   ├── victoriametrics
│   │   ├── files
│   │   ├── tasks
│   │   ├── templates
│   │   └── vars
│   └── wireguard
│       ├── handlers
│       │   └── main.yml
│       ├── meta
│       │   └── main.yml
│       ├── tasks
│       │   ├── client.yml
│       │   └── server.yml
│       └── templates
│           ├── add-nat-rules.sh.j2
│           ├── client.conf.j2
│           ├── remove-nat-rules.sh.j2
│           └── wg0.conf.j2
├─ templates
│   ├── haproxy
│   │   └── haproxy.cfg.j2
│   ├── podman
│   │   └── storage.conf.j2
│   └── traefik
│       ├── conf.d
│       │   ├── anubis.yml.j2
│       │   ├── authelia.yml.j2
│       │   ├── crowdsec.yml.j2
│       │   ├── dashboard.yml.j2
│       │   ├── jellyfin.yml.j2
│       │   ├── sws.yml.j2
│       │   ├── middlewares.yml.j2
│       │   └── ...
│       └── traefik.yaml.j2
├── traefik-config.yml
├── traefik.yml
├── jellyfin.yml
├── uptimekuma.yml
├── victoria-metrics.yml
├── vm-agent.yml
├── vm-alertmanager.yml
├── vm-alert.yml
├── website-deploy.yml
├── wg-gwt-client.yml
├── wg-hetzner-server.yml
├── wg-oracle-client.yml
└── ...

files/

Any flat config file, generated asset or image. Basically anything without injected variables, however, these folders might also reflect the logical grouping for configs for that particular service (ie rules/ for monitoring).

templates/

Config files with injected variables used directly by playbooks. Otherwise they belong in the appropriate role’s templates/ dir. The only weird, non-standard thing I do here is keep each Traefik service config separate. Its just easier to see at a glance the current state of whats deployed and how its configured instead of wading through giant blocks of label definitions.

group_vars/

My rule of thumb is to scope variables as narrowly as possible because it quickly becomes a nightmare trying to figure out precedence and where values are coming from. The only exception in this case is vault_passwords.yml where I encrypt all the secrets in one place.

Here I’ll also store variables for different services (which could be across different hosts) in separate .yml files, or variables that are relevant for an entire network or site.

host_vars/

Variables specific to each physical host or VPS.

roles/

Ideally everything should be consolidated into a role. My typical workflow usually starts with writing everything in a single playbook, then if I have the patience or free time extract and generalize as much as possible into a role. Clearly that has not fully transpired judging by the 20+ dangling playbooks for all my self-hosted stuff but then again every project is a WIP :)

Welcome to my digital domain


It's the bees knees.

By rpavlov, 2025-06-12