Requirements and environments
What a script needs from Galaxy and from the machine that runs it, and three ways to have it: an environment built with uv, a container, or a community base.
Galaxy requirements#
requires_role("geerlingguy.postgresql", version="3.5.2")
requires_collection("community.general", version=">=8.0.0")
They become requirements.yml, and install (or run --install) puts
them in roles/ and collections/ inside the generated tree, where
ansible finds them without any configuration. That is the only place they
go: an environment (below) never holds them.
The tree’s ansible.cfg, yours or instamix’s, gets collections_path = ./collections and roles_path = ./roles when it sets none, so galaxy
content stays in the tree (the ansible-cfg.local-paths setting; on by
default). install goes where the first of each points, and refuses to run
when that is outside the tree. It counts what the environment in use has,
so a dependency the environment holds isn’t installed again, but not what
the host has installed.
When ansible runs, that ansible.cfg alone says where it searches:
ANSIBLE_COLLECTIONS_PATH and ANSIBLE_ROLES_PATH, your own included, are
left out of its environment once the file names the paths, so ~/.ansible
and the system directories only count if it says so.
Runtime requirements#
What the machine that runs the playbook needs:
requires_ansible(">=2.16,<2.19") # ansible-core
requires_python(">=3.11") # the Python ansible runs on
requires_pip("jmespath", "netaddr>=0.10")
requires_command("rsync", "ssh")
doctor checks them against this machine. run, install, test and
lint refuse to start when one is missing, and name it;
--skip-requirements runs anyway. Versions and packages are looked up in
the Python that ansible-playbook runs on, which is not necessarily the
one running the script. Pip requirements also go into a
requirements.txt, unless you declare one. Like everything else, they
respect profiles and only=.
Environments#
env builds that controller with uv: a
virtualenv on the Python requires_python() asks for, holding
ansible-core and the pip requirements, resolved into a lock with hashes
and installed from it with --require-hashes. It prints the directory.
$ python site.py env
~/.cache/instamix/envs/af699b99d7fec240
instamix: built ansible-core 2.17.14 on Python 3.12.13
$ python site.py run --env # also install, lint, test and doctor
--env (or INSTAMIX_ENV=1) builds the environment if needed and puts
its bin/ first on PATH for everything instamix starts; without it,
ansible comes from PATH as before.
Environments live under ~/.cache/instamix/envs/, named by a digest of
what they hold, so scripts with the same requirements share one and a
change in them makes a new one. env --dir DIR builds somewhere else;
env --upgrade resolves again instead of reusing the lock. Each holds
venv/, requirements.in, requirements.lock and env.json.
requires_command() is only checked: uv cannot provide programs outside
Python.
An environment holds galaxy content only when a base (below) brings it: a
script’s own collections and roles go into its tree. A base’s go in with
the environment’s own ansible-galaxy, into its collections/ and
roles/, through an ansible.cfg of its own: what the host has
installed, and the host’s ansible.cfg, don’t count.
When a tree is built for an environment with --outside-paths (run --env --outside-paths, off by default), its ansible.cfg also names the
environment’s collections/ and roles/ in collections_path and
roles_path, after the tree’s own (or before ansible’s defaults, with
ansible-cfg.local-paths off), so ansible-playbook run in the tree by
hand finds them, and --install doesn’t install what the environment has
again. Without it, only the tree counts: everything the playbook needs
goes into the tree.
Containers#
imix container SCRIPT [--image IMAGE] [OPTIONS] [PLAYBOOK] [-- ARGS]
(also python site.py container ...) runs the playbook in a container
instead: the tree is built here and mounted at /runner/project, and only
ansible runs inside – ansible-galaxy for --install, then
ansible-playbook – so any execution environment image will do. It takes
run’s options.
The image is --image, else the script’s container_image("..."), else
its base’s (requires_base("base") means
ghcr.io/ansible-community/community-ee-base:latest); python site.py image prints which, and why. The community images are meant for
development and CI, not production.
The container gets the host’s network (hosts and the proxy are reached as
from here), the SSH agent, ~/.ssh read-only, and, read-only, the files
outside the tree that -i (with the group_vars/ and host_vars/ next
to it), --vault-password-file and -e @FILE name; those arguments are
rewritten to where the container sees them. Paths after -- are passed as
they are.
It runs with podman, else docker (--engine picks one); with podman the
image’s user is you (--userns=keep-id), and SELinux labelling is off for
it so ~/.ssh is readable without relabelling. The tree’s ansible.cfg
also names the image’s own collections and roles, after the tree’s, so
what the image ships counts and isn’t installed again (--outside-paths,
on by default here; --no-outside-paths for a tree that holds everything
itself).
Its options are its own, so settings for it go in a [container]
section: install = true there doesn’t come from [run]. --pull says
when to pull the image (default: when missing).
Bases#
requires_base("minimal") or requires_base("base") starts the
environment from what the community execution environments
(ansible-community/images)
put on the controller: their Python, their packages as they pin them
(ansible-core among them), and their collections (base: ansible.posix,
ansible.utils and ansible.windows; minimal: none).
The environment then holds the base alone, so scripts on the same base
share one; a script’s own requires_collection() and requires_role() go
into its tree (run --install), as always. requires_ansible() and
requires_python() narrow the base’s pins, and requires_pip() adds to
them.
requires_base("base")
requires_collection("community.general") # in the tree, with --install
The pins live in instamix_bases.py; tools/update_bases.py [--ref TAG]
writes it again from the images repository.