Open-Source Wikis

/

Ansible

/

Systems

/

Module execution and AnsiBallZ

ansible/ansible

Module execution and AnsiBallZ

Ansible doesn't run a daemon on remote hosts. Each task that needs a module to run is built into a small, self-contained Python archive ("AnsiBallZ"), shipped to the target, executed once, and discarded. This page explains how the archive is built, what it contains, and how the controller and target communicate.

Files

File Lines Purpose
lib/ansible/executor/module_common.py 1,694 Top-level builder: discovers module_utils deps, invokes the AnsiBallZ builder, returns a payload
lib/ansible/_internal/_ansiballz/_builder.py (medium) Constructs the actual zip archive
lib/ansible/_internal/_ansiballz/__init__.py (small) Public surface
lib/ansible/module_utils/basic.py 2,219 Remote-side AnsibleModule class — argument parsing, JSON I/O, file handling, no-log scrubbing
lib/ansible/module_utils/_internal/ (multiple) Embedded support code (datatag, dataclass validation, JSON helpers) shipped with modules
lib/ansible/executor/powershell/module_manifest.py (medium) PowerShell equivalent for Windows targets
lib/ansible/executor/powershell/ (multiple) The PowerShell wrapper script and helpers
lib/ansible/_internal/_powershell/ (multiple) Internal PowerShell glue (clixml parsing, script generation)

What's in an AnsiBallZ archive

An AnsiBallZ for a Python module is a zipfile (Python's built-in zipfile is sufficient) with this layout:

__main__.py                 # Bootstrap: unpack, set up sys.path, invoke main()
ansible/__init__.py
ansible/module_utils/
    __init__.py
    basic.py                # Always shipped
    common/                 # Always shipped (subset)
    _internal/              # Always shipped (subset)
    <other module_utils picked up by AST analysis>
ansible/modules/<module_name>.py
ANSIBALLZ_PARAMS            # JSON-encoded args for this invocation

The archive is base64-encoded into a Python script that exec()s its decoded zip via Python's standard zip-import support. The result: a single Python file that, when run on the target, materializes its dependencies and runs the module.

The build flow

graph TD
    REQ[Action plugin: _execute_module] --> MMOD[modify_module<br/>module_common.py]
    MMOD --> AST[AST-walk module source]
    AST --> COLLECT[Collect imported<br/>ansible.module_utils.* paths]
    COLLECT --> RECURSE[Recurse: for each module_util,<br/>walk its imports too]
    RECURSE --> BUILD[ZipFile builder<br/>_ansiballz/_builder.py]
    BUILD --> EMBED[Embed serialized module<br/>+ helpers + ANSIBALLZ_PARAMS]
    EMBED --> WRAP[Wrap in __main__ Python script]
    WRAP --> RETURN[Return shipped Python source]

Step 1: AST-walking the module's imports

lib/ansible/executor/module_common.py:_find_module_utils and friends parse the module's source with Python's ast module, collect every import ansible.module_utils.X and from ansible.module_utils.X import Y, and recurse — walking the import graph until it has the closure of every module_utils file the module needs.

It also detects collection imports: from ansible_collections.community.general.plugins.module_utils.helper import foo triggers a lookup through the collection loader, and that file gets bundled too.

Step 2: Building the zip

_ansiballz/_builder.py constructs an in-memory zipfile.ZipFile and writes:

  • The module source (with from __future__ import annotations and the YAML doc strings stripped if not needed at runtime).
  • Every collected module_utils file at the right path inside the archive.
  • A small, fixed bootstrap __main__.py.
  • The serialized arguments as ANSIBALLZ_PARAMS.
  • Side-channel data needed for tagged values, vault decryption, and serialized Origin info.

Step 3: The Python wrapper

The zip is base64-encoded and embedded inside a generated Python script that:

  1. Decodes the base64 zip into BytesIO.
  2. Inserts it onto sys.path via zipimport.
  3. Imports ansible.modules.<module_name> and calls its main().
  4. Writes the JSON result to stdout.
  5. On any error, captures the traceback into a JSON failed: true result.

This wrapper is what gets transferred to the remote target.

Step 4: Connection plugin transfer

The action plugin (typically lib/ansible/plugins/action/normal.py or a module-specific override) hands the wrapper to the connection plugin's put_file(). The connection plugin (ssh, winrm, psrp, local) writes the file to a per-task temporary directory under ansible_remote_tmp (default ~/.ansible/tmp/).

It then runs the wrapper through the connection's exec_command(), with a wrapping shell that may include become (sudo/su/runas) and shell quoting from the shell plugin.

Step 5: Cleanup

After the JSON result is parsed, the action plugin issues a delete via the connection plugin to remove the temp file. If ANSIBLE_KEEP_REMOTE_FILES=1, the cleanup is skipped — useful for debugging.

AnsibleModule on the target

lib/ansible/module_utils/basic.py:AnsibleModule is the remote-side counterpart. Every module's main() instantiates it:

module = AnsibleModule(
    argument_spec=dict(
        path=dict(type='str', required=True),
        state=dict(type='str', choices=['present', 'absent'], default='present'),
    ),
    supports_check_mode=True,
)

AnsibleModule.__init__ (the bulk of the 2,219 lines):

  • Reads ANSIBALLZ_PARAMS from the wrapper.
  • Validates arguments against argument_spec via lib/ansible/module_utils/common/parameters.py.
  • Sets up no-log scrubbing for parameters marked no_log=True.
  • Configures temp-file handling, locale, debugging.
  • Wires up module.exit_json(), module.fail_json(), module.warn(), module.deprecate().

The module's main() then does its work, calls module.exit_json(...) or module.fail_json(...), and the JSON makes its way back to the controller.

Argument validation

argument_spec is a dict-of-dicts schema. Every key is a parameter name, and every value declares:

  • typestr, int, bool, list, dict, path, raw, etc.
  • required, default, choices, no_log, aliases.
  • elements (the type of each list element).
  • Cross-parameter constraints: required_if, required_together, mutually_exclusive, required_one_of.

The validator (lib/ansible/module_utils/common/arg_spec.py:ArgumentSpecValidator) operates entirely on the target side, so collection authors can use it independently. It returns a structured result that's also useful for testing.

PowerShell on Windows targets

For Windows targets connecting via winrm or psrp, the equivalent flow uses PowerShell:

  • lib/ansible/executor/powershell/module_manifest.py builds an "exec wrapper" PowerShell script.
  • lib/ansible/executor/powershell/exec_wrapper.ps1 is the boilerplate shipped to the target.
  • C# helpers under lib/ansible/module_utils/csharp/ (Ansible.Basic.cs, Ansible.Process.cs, ...) are compiled by PowerShell at runtime to give Windows modules an equivalent of AnsibleModule.
  • Windows modules are typically .ps1 files; some are .py modules that run via PowerShell's Python invocation, but the dominant pattern is .ps1.

lib/ansible/_internal/_powershell/_clixml.py parses the CLIXML output streams that PowerShell remoting uses.

Why a zip archive?

Earlier Ansible used a different format ("old-style modules" and "ANSIBALLZ"). The current zip-archive approach has several wins:

  • Hermetic. A module imports its module_utils cleanly — no file-system dependencies on the target, no module_utils path tricks.
  • Pythonic. zipimport is stdlib; no special bootstrap on the target.
  • Inspectable. You can unzip -l an AnsiBallZ archive to see exactly what was shipped.
  • Cacheable. Action plugins can reuse a built archive across multiple invocations of the same module on the same host (with ANSIBLE_PIPELINING).

Pipelining

When ANSIBLE_PIPELINING=true, the wrapper isn't transferred with put_file; it's piped to the remote Python interpreter over the same SSH session that started the command. This avoids the temp-file write/read cycle. The trade-off: pipelining is incompatible with sudo configurations that require a TTY (requiretty), so it's off by default.

Integration points

  • Imported by: every action plugin via lib/ansible/plugins/action/__init__.py:ActionBase._execute_module.
  • Imports: lib/ansible/plugins/loader.py (to find module_utils), lib/ansible/parsing/yaml/loader.py (to parse the module's DOCUMENTATION), the entire module_utils tree.
  • Caches: per-module-source-hash compiled wrappers, so re-running the same module against many hosts only does the AST walk once.

Entry points for modification

  • Changing what's shipped to the target — edit module_common.py:_find_module_utils (recursion logic) or _ansiballz/_builder.py (archive layout).
  • Adding a target-side helper — drop it into lib/ansible/module_utils/_internal/ if it's controller/target-shared, or into module_utils/common/ for module-author use.
  • PowerShell wrapper changeslib/ansible/executor/powershell/ and lib/ansible/module_utils/powershell/.
  • Debugging an AnsiBallZ build — set ANSIBLE_KEEP_REMOTE_FILES=1, ssh to the target, and unzip -l ~/.ansible/tmp/.../AnsiballZ_<module>.py to inspect the archive.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Module execution and AnsiBallZ – Ansible wiki | Factory