Skip to main content

Migrating from Python ansible-core to ansible-core-rs

ansible-core-rs is a Rust reimplementation of ansible-core. It aims to run existing playbooks, inventories, and vault files without modification while reducing startup time, memory use, and task-execution overhead. This guide describes what is compatible today, where behavior differs, and how to move an existing Ansible project onto the Rust engine.

Design stance

ansible-core-rs is a drop-in replacement for the engine, not for the module ecosystem. The execution engine, playbook loader, inventory, vault, Galaxy client, and template renderer are Rust. Modules remain Python: common modules (command, file, copy, apt, service, …) have native Rust implementations for speed, and everything else runs through the AnsiballZ Python bridge exactly as upstream Ansible does.

This means your collections, roles, and modules — including anything you install from Galaxy — continue to work. Your playbooks do not need to change.

Installation

Source build

git clone <repo>
cd ansible-core-rs
cargo build --release
./target/release/ansible --version

A Python 3.12+ interpreter must be on PATH for modules that fall back to AnsiballZ.

Nix

nix develop     # dev shell with rust, python, openssh, etc.
nix build       # builds the `ansible` binary

PyPI wrapper (planned)

A pip install ansible-core-rs package that bundles the Rust binary is planned for Phase 11 and not yet available.

Command-line differences

Upstream ansible-core ships nine separate binaries (ansible, ansible-playbook, ansible-vault, …). ansible-core-rs ships a single ansible binary with subcommands:

Upstream ansible-core-rs
ansible ansible <pattern>
ansible-playbook ansible playbook
ansible-vault ansible vault
ansible-config ansible config
ansible-inventory ansible inventory
ansible-galaxy ansible galaxy
ansible-doc ansible doc
ansible-pull ansible pull

The single binary already dispatches on argv[0] (crates/ansible-core/src/main.rs), so a symlink named ansible-playbook, ansible-vault, etc. runs the matching subcommand directly — the legacy names work as drop-in replacements, no wrapper needed:

for name in ansible-playbook ansible-vault ansible-config \
            ansible-inventory ansible-galaxy ansible-doc ansible-pull; do
  ln -s ansible /usr/local/bin/$name
done

Common flags (-i, -e, -l, --tags, --skip-tags, --start-at-task, --check, --diff, -v, -b/--become, -u, -f/--forks, --vault-password-file) match upstream behavior.

Configuration

ansible.cfg loading uses the same precedence as upstream:

  1. ANSIBLE_CONFIG environment variable
  2. ./ansible.cfg in cwd
  3. ~/.ansible.cfg
  4. /etc/ansible/ansible.cfg

Environment variables with the ANSIBLE_ prefix override config file settings identically.

View the effective config:

ansible config dump
ansible config list
ansible config view

Inventory

  • INI inventories: fully supported, including group children, group vars, host vars, ranges (web[01:05].example.com), and [group:vars] sections.
  • YAML inventories: fully supported.
  • Script/dynamic inventories: supported. Executable files in the inventory path are invoked with --list / --host <name> exactly like upstream.
  • Plugin inventories (e.g. aws_ec2, gcp_compute): not yet bridged through PyO3 — see the "Python plugins" section below.

ansible inventory --list and ansible inventory --graph produce structurally equivalent output to upstream. The JSON key ordering may differ since Rust's HashMap is unordered; if a consumer depends on specific ordering, pipe through jq --sort-keys.

Large inventories benefit from the interned-name representation: every host and group name is stored once as an Arc<str> and shared between the inventory map, membership lists, and reverse lookups.

Playbooks

Playbook YAML loading supports plays, tasks, blocks (with rescue / always), handlers, roles, includes, and imports. pre_tasks, post_tasks, and role dependencies resolve identically to upstream.

Variable precedence

All 22 precedence levels from upstream are implemented. Exhaustive parity testing against the Python implementation is still in progress (Phase 9); if you hit a case where a variable resolves differently, please open an issue with a minimal reproduction.

Tags

--tags, --skip-tags, and the special tags always, never, all, tagged, untagged behave identically. Inheritance from block → task is preserved.

Loops

loop, with_items, loop_control (including label, pause, index_var, extended and extended_allitems), and register aggregation into results[] all work. until / retries / delay work. The extended loop vars match upstream: ansible_loop.index is 1-based, index0 is 0-based, and allitems / nextitem / previtem are present under extended: true. ansible_loop_var (and ansible_index_var, when index_var: is set) are exposed on every iteration.

Modules

Native Rust modules (no Python spawn)

These run entirely in-process on the control node, skipping the AnsiballZ wrapper. They exist to eliminate per-task Python startup overhead for the most common task types:

  • Core: debug, set_fact, assert, fail, meta
  • Command: command, shell, raw
  • Files: file, copy, stat, template, lineinfile, blockinfile, replace, slurp
  • Network/HTTP: uri, get_url, wait_for_connection (under the http feature)
  • Facts: setup, gather_facts, ping, package_facts
  • Package managers: apt, yum, dnf
  • Services: service, systemd, systemd_service
  • Users/groups: user, group
  • Includes: include_role, import_role, include_tasks, import_tasks

Argument names and behavior follow upstream module documentation. Where this guide is silent, assume upstream semantics.

Non-parity extensions

Six native modules have no counterpart in upstream ansible-core at all:

Module Upstream status
cargo ships in community.general, not ansible-core
bun, uv, pixi, paru, pkg exist nowhere in Ansible

They are deliberate extensions, not parity work. The operator-facing consequence is the important part: a playbook that uses one of these will not run on upstream ansible-playbook at all. Upstream resolves module names at parse time, so it aborts the whole playbook before the first task:

[ERROR]: couldn't resolve module/action 'cargo'. This often indicates a
misspelling, missing collection, or incorrect module path.

(exit 4, zero tasks executed). If you need a playbook to run on both engines, use command/shell, or community.general.cargo where it applies.

For the same reason the differential oracle cannot vouch for these modules — it compares two engines, and upstream never gets as far as running them. They are excluded from the parity coverage: denominator and reported as their own category, so the number is not quietly diluted by modules whose coverage can never improve. Their evidence is unit tests over the argv builders instead. See issue #558.

Everything else

Any module name not in the list above is routed through the AnsiballZ fallback: the module source is packaged into a self-extracting Python script and executed on the target host exactly as upstream does. This means modules shipped in collections — community.general.*, ansible.posix.*, cloud modules, and so on — run unmodified.

The AnsiballZ packager produces wrappers that are byte-compatible with the Python implementation.

Templates

MiniJinja provides the template engine. 74 filters and 15 tests are implemented natively, covering the core Ansible filter set plus frequently used extras (regex, type tests, version, subset/superset, filesystem).

Complex templates that use Jinja2-specific features not in MiniJinja will fall back to a PyO3-hosted Jinja2 environment (planned — see Phase 7 in the migration plan). Until that fallback lands, the following edge cases may render differently:

  • Custom Jinja2 filters defined in plugins (none of the built-ins are missing, but collection-provided filters don't load yet)
  • Obscure extension tags ({% raw %} and the standard control flow tags all work; exotic third-party extensions do not)

If you hit a template that doesn't render, please report it.

Vault

ansible vault encrypt, decrypt, view, edit, rekey, and encrypt-string all work. The binary format is interoperable with upstream — files encrypted by Python ansible can be decrypted by ansible-core-rs and vice versa. AES-256-CTR and the PBKDF2 key derivation match upstream parameters.

Vault secrets use the zeroize crate to wipe key material from memory after use.

Galaxy

ansible galaxy collection install, build, and list work against local mirrors via the filesystem fetcher. The Galaxy v3 API fetcher is behind the network Cargo feature and has been exercised against local test doubles; it has not yet been validated against galaxy.ansible.com in CI (blocked on toolchain availability — see the migration plan).

requirements.yml batch installs, dependency resolution (depth-first with constraint tightening), and FQCN plugin resolution all match upstream.

Python plugins

This is the biggest current gap. PyO3 is wired into the build, but the plugin loader that discovers and executes Python callback, lookup, filter, and inventory plugins is partial:

  • Callback plugins: the default callback produces upstream-equivalent output (TASK banners, PLAY RECAP, color, diff rendering, verbose levels). Third-party callback plugins do not yet load.
  • Lookup plugins: not yet bridged. {{ lookup('file', ...) }} and the other built-in lookups work via native implementations, but custom lookups from collections do not yet execute.
  • Filter/test plugins: the 74 native filters cover the ansible-core built-in set; collection-provided filter plugins do not yet load.
  • Inventory plugins: dynamic inventory scripts work; the plugin interface (used by aws_ec2, etc.) does not yet load.

If your playbooks depend on collection-provided plugins of these types, you'll want to stay on Python ansible-core until Phase 6 completes. Module plugins from collections do work — the gap is only for non-module plugin types.

SSH

OpenSSH via the ssh CLI is the default and only connection plugin today. ControlMaster multiplexing is enabled by default for both ssh and scp/ sftp invocations: the first task per host opens a master connection that subsequent tasks reuse via a derived ControlPath, with ControlPersist=60s keeping it warm across the play. The control socket is closed on disconnect via ssh -O exit. Override the socket location by setting ansible_ssh_common_args if your environment needs a non-default path.

sshpass is used for password authentication when ansible_ssh_pass is set; use key-based auth or the SSH agent for production.

Performance characteristics

Measured on the benchmark suite (run cargo bench -p ansible-core):

Operation ansible-core-rs Notes
Binary startup ~3.3 ms vs. ~300 ms for Python import overhead
Playbook parsing 24–122 µs Depends on playbook size
Template rendering 1.5–4.7 µs Simple to complex expressions
Inventory parsing ~65 µs 1000-host fixture

A full parity comparison against Python ansible-core on identical workloads is pending (requires both runtimes on the same CI host).

Debugging behavioral differences

If a playbook produces different output under ansible-core-rs:

  1. Run with -vvv on both implementations and diff the output.
  2. Check that any custom plugins used are in the "native" or "AnsiballZ" lists above — if they're plugin types not yet bridged, that's the cause.
  3. Confirm that your ansible.cfg is being read (ansible config dump shows the effective values).
  4. For variable precedence issues, dump host vars: ansible inventory --host <name>.
  5. File an issue with the playbook excerpt, relevant inventory, and both -vvv outputs.

When to hold off

Today's ansible-core-rs is best suited for:

  • Playbooks whose tasks use the native modules or common collection modules (AnsiballZ handles these)
  • Inventories from INI, YAML, or dynamic scripts
  • Default callback output
  • Vault workflows

Stay on Python ansible-core for now if your workflow relies on:

  • Custom callback plugins beyond the default
  • Custom lookup / filter / test plugins from collections
  • Plugin-style dynamic inventories (aws_ec2, gcp_compute, …)
  • ansible-test for sanity/unit/integration testing (Phase 9 WIP)

Roadmap

The major open items, in roughly priority order, are tracked in MIGRATION_PLAN.md. Highlights:

  • Phase 6 PyO3 bridge: loading non-module Python plugins
  • Phase 10.1: concurrent collection downloads (SSH multiplexing landed)
  • Phase 11: Docker image / PyPI wrapper (Nix flake builds today)

Contributions and bug reports are welcome.

Metadata

Release files for ansible-core-rs 0.16.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ansible-core-rs 0.16.0
File Size Uploaded Provenance Actions
ansible_core_rs-0.16.0.tar.gz 1.0 MB Without provenance Details for ansible_core_rs-0.16.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for ansible-core-rs 0.16.0
File Interpreter ABI Platform Actions
ansible_core_rs-0.16.0-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details for ansible_core_rs-0.16.0-py3-none-manylinux_2_28_x86_64.whl
ansible_core_rs-0.16.0-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details for ansible_core_rs-0.16.0-py3-none-manylinux_2_28_aarch64.whl

Total release size: 16.2 MB

Release files / ansible_core_rs-0.16.0.tar.gz

Download URL ansible_core_rs-0.16.0.tar.gz
Size 1.0 MB
Tags Source
SHA-256 checksum
How to use checksums
01f71b4469f4497f6ff9ea8ec25a74713e2600d17c1c154ecd457ca745fd7f7c
BLAKE2b-256 checksum
How to use checksums
bebcc51666c50e21fa0df26c7604c2212d6abf6d88b4dd5357776ca7952f9616
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / ansible_core_rs-0.16.0-py3-none-manylinux_2_28_x86_64.whl

Download URL ansible_core_rs-0.16.0-py3-none-manylinux_2_28_x86_64.whl
Size 7.8 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
8156782204276a31833504dc6d2f0651c23c7b3abb8918a204490bf8f543aebe
BLAKE2b-256 checksum
How to use checksums
a1e151400f6cc23c4537de5a6428e30514f85004fb9dff004d98e5a5c2f10581
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / ansible_core_rs-0.16.0-py3-none-manylinux_2_28_aarch64.whl

Download URL ansible_core_rs-0.16.0-py3-none-manylinux_2_28_aarch64.whl
Size 7.4 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
46adf15686575f11f4d20deb4df0a3e0eafb93ea51547453600dfb191c32f664
BLAKE2b-256 checksum
How to use checksums
2aca9a13d8085d675cc9dd0f7716d40b3b973a6c618643fb39b4e485f814058e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for ansible-core-rs 0.16.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page