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 invocationThe 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 annotationsand the YAML doc strings stripped if not needed at runtime). - Every collected
module_utilsfile 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
Origininfo.
Step 3: The Python wrapper
The zip is base64-encoded and embedded inside a generated Python script that:
- Decodes the base64 zip into
BytesIO. - Inserts it onto
sys.pathviazipimport. - Imports
ansible.modules.<module_name>and calls itsmain(). - Writes the JSON result to stdout.
- On any error, captures the traceback into a JSON
failed: trueresult.
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_PARAMSfrom the wrapper. - Validates arguments against
argument_specvialib/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:
type—str,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.pybuilds an "exec wrapper" PowerShell script.lib/ansible/executor/powershell/exec_wrapper.ps1is 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 ofAnsibleModule. - Windows modules are typically
.ps1files; some are.pymodules 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_utilscleanly — no file-system dependencies on the target, no module_utils path tricks. - Pythonic.
zipimportis stdlib; no special bootstrap on the target. - Inspectable. You can
unzip -lan 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'sDOCUMENTATION), the entiremodule_utilstree. - 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 intomodule_utils/common/for module-author use. - PowerShell wrapper changes —
lib/ansible/executor/powershell/andlib/ansible/module_utils/powershell/. - Debugging an AnsiBallZ build — set
ANSIBLE_KEEP_REMOTE_FILES=1, ssh to the target, andunzip -l ~/.ansible/tmp/.../AnsiballZ_<module>.pyto inspect the archive.
Cross-links
- Executor — what triggers the build.
- Plugins → Action — the action plugin layer that calls into module_common.
- Plugins → Connection — how the wrapper actually crosses the wire.
- Plugins → Become — the wrapping that runs the wrapper as a different user.
- Features → Modules and actions — the user-facing view.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.