Sign in
← Ansible
12 min · medium · Lesson 4 of 12

Running without Python on the host

A managed node usually needs Python. The raw module, gather_facts: no, bootstrapping Python, and the script module.

Why this matters

Ansible reaches a managed node over SSH and doesn't install anything there ahead of time. But almost every module on that side runs through a Python interpreter. On a fresh minimal image, in an Alpine or busybox container, Python might not be there at all, and the playbook fails on the very first task. This lesson covers why that happens and how to work with such a host: the raw and script modules, disabling fact gathering, and installing Python through Ansible itself.

Why Ansible needs Python on the host

Most Ansible modules are written in Python. When you run a task, Ansible packs the module together with its arguments into a single file, copies it to the managed node, and runs it there. The chain looks like this:

  1. The control node builds a Python module with its arguments.
  2. It copies it to the host over SSH (SFTP or SCP) into a temporary folder ~/.ansible/tmp/ of the user it connects as.
  3. It runs it with whatever interpreter it finds on the host, usually /usr/bin/python3. Ansible looks for the path itself (interpreter discovery), and you can set it with the ansible_python_interpreter variable.
  4. It gets a JSON response back and removes the temporary files.

If the host has no Python, step 3 fails right away. That happens with fresh minimal images, containers without Python, and network hardware.

control node ansible regular module copies .py module.py no python3 task failed! raw SSH directly bash / sh no Python needed module raw

Here's what that looks like on Alpine with no Python. The playbook fails before the first task, during fact gathering:

TASK [Gathering Facts] *********************************************************
[WARNING]: No python interpreters found for host 'web1' (tried ['python3.13', ...,
'/usr/bin/python3', 'python3']).
[ERROR]: Task failed: Action failed: The following modules failed to execute: ansible.legacy.setup.
...
| The module interpreter '/usr/bin/python3' was not found. Consider overriding the configured interpreter path for this host. See stdout/stderr for the returned output.
...
fatal: [web1]: FAILED! => {... "module_stdout": "/bin/sh: /usr/bin/python3: not found\r\n", ... "rc": 127}, "msg": "The following modules failed to execute: ansible.legacy.setup."}

PLAY RECAP *********************************************************************
web1                       : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

The key part of the message is module interpreter ... was not found and rc: 127: the shell on the host couldn't find an interpreter. SSH itself works fine, and the key was accepted.

The raw module

raw runs a command directly over SSH, without any Python modules on the other side. Ansible just sends a string to the host's shell and collects the output.

ansible all -m raw -a "uptime"
ansible all -m raw -a "cat /etc/os-release"

When you need raw:

For network hardware (Cisco IOS, Junos) the standard path is different: connection: network_cli and modules from the vendor's collections. They run on the control node and talk to the device over SSH. raw is still a fallback there.

raw understands regular inventory variables and connection parameters (ansible_host, ansible_user, ansible_port, become).

gather_facts: false

Before running tasks, Ansible gathers facts about the host: it runs the setup module, which finds out the OS, IP addresses, disks, and so on. That module needs Python too.

On a host with no Python, fact gathering fails before the first task (see the output above). The fix: disable it at the play level.

- name: bootstrap host without Python
  hosts: newservers
  gather_facts: false    # Python isn't installed yet
  tasks:
    - name: check connectivity
      ansible.builtin.raw: uptime

no works the same as false, and both show up in the docs. In this course we write false. Once Python is installed, you can gather facts explicitly with a task using the setup module.

Bootstrapping Python via raw

A classic trick: install Python first with raw, then work with regular modules from there on.

- name: bootstrap Python3 on Debian/Ubuntu
  hosts: newservers
  gather_facts: false
  become: true
  tasks:
    - name: install python3
      ansible.builtin.raw: test -e /usr/bin/python3 || (apt-get update && apt-get install -y python3)
      changed_when: false

    - name: gather facts
      ansible.builtin.setup:

    - name: install nginx
      ansible.builtin.apt:
        name: nginx
        state: present

test -e /usr/bin/python3 || keeps a rerun from hitting the repository again. changed_when: false removes the false changed, more on that below.

After setup, the ansible_facts.* variables become available, and regular modules work as usual. On RHEL, Rocky, and AlmaLinux the package is installed with dnf install -y python3, on Alpine with apk add python3.

The script module

script also works with no Python on the host. It copies a local script from the control node to the host and runs it there through a shell. The interpreter is set by the shebang on the script's first line, or by the executable parameter.

- name: run init script
  ansible.builtin.script: files/bootstrap.sh
  args:
    creates: /etc/myapp/.bootstrapped   # skip if the file already exists

creates checks a path on the host: if the file is there, the task is skipped with status skipped. The opposite parameter, removes, runs the script only if the file is there.

How script differs from raw:

Don't confuse these with command and shell: those are regular modules, and they need Python on the host.

The honest take on raw and script

raw and script are for edge cases. They have no idea whether anything on the host actually changed. On a zero return code, raw always reports changed, even if the command changed nothing. On a non-zero code, the task fails with failed and a non-zero return code message. So for check-style commands you set changed_when: false by hand, and for script you set creates or removes.

That's exactly why they're used sparingly: to install Python, initialize a host, do something there's no regular module for. After that, regular modules take over, since they check the current state on their own.

Common mistakes

What to remember

Ansible modules run on the host through a Python interpreter. No Python, no regular modules, no fact gathering.

raw runs a command over SSH without Python. gather_facts: false turns off fact gathering, which also needs Python.

Bootstrapping via raw: first install python3, then run setup, then work with regular modules.

script copies a local script and runs it with no Python on the host. raw and script aren't idempotent, so they're used sparingly, not as a replacement for regular modules.

In the next lesson we'll cover idempotency and why a playbook made of plain shell commands works poorly.

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson ansible-basics-04-without-python