Introduction
Most Ansible modules require Python on the remote host. ansible.builtin.raw bypasses this requirement entirely — it sends commands directly over SSH (or the configured connection) without any module framework. This makes it essential for three scenarios: bootstrapping Python on new systems, managing network devices without Python, and interacting with minimal/embedded systems.
Basic Usage
---
- name: Raw module examples
hosts: all
gather_facts: false # Must disable — gather_facts needs Python!
tasks:
- name: Run a simple command
ansible.builtin.raw: uptime
- name: Run with pipe
ansible.builtin.raw: cat /etc/hostname | tr '[:lower:]' '[:upper:]'
register: hostname_upper
- name: Show result
ansible.builtin.debug:
msg: "{{ hostname_upper.stdout_lines }}"
Bootstrap Python
The most common use case — install Python on a fresh system so other modules work:
- name: Bootstrap new servers
hosts: new_servers
gather_facts: false # Can't gather facts without Python
tasks:
# Debian/Ubuntu
- name: Install Python on Debian
ansible.builtin.raw: |
apt-get update -qq && apt-get install -y -qq python3 python3-apt
become: true
changed_when: true
# RHEL/CentOS/Rocky
- name: Install Python on RedHat
ansible.builtin.raw: |
dnf install -y python3
become: true
changed_when: true
# Alpine Linux
- name: Install Python on Alpine
ansible.builtin.raw: |
apk add --no-cache python3
become: true
# Now gather facts
- name: Gather facts after Python is available
ansible.builtin.setup:
- name: Continue with normal modules
ansible.builtin.package:
name: curl
state: present
Auto-Detect and Bootstrap
- name: Universal Python bootstrap
hosts: all
gather_facts: false
tasks:
- name: Check if Python exists
ansible.builtin.raw: command -v python3 || command -v python
register: python_check
failed_when: false
changed_when: false
- name: Install Python (Debian)
ansible.builtin.raw: apt-get update && apt-get install -y python3
become: true
when: python_check.rc != 0
register: install_result
failed_when: install_result.rc != 0
- name: Gather facts
ansible.builtin.setup:
when: python_check.rc == 0 or install_result is changed
Network Device Management
- name: Configure network switches
hosts: switches
gather_facts: false
connection: network_cli # or paramiko
tasks:
- name: Show running config
ansible.builtin.raw: show running-config
register: config
- name: Show interfaces
ansible.builtin.raw: show ip interface brief
register: interfaces
- name: Configure VLAN
ansible.builtin.raw: |
configure terminal
vlan 100
name production
exit
exit
Docker Container Bootstrap
- name: Bootstrap Docker containers
hosts: containers
gather_facts: false
connection: community.docker.docker
tasks:
- name: Install Python in container
ansible.builtin.raw: |
apt-get update && apt-get install -y python3
changed_when: true
- name: Now use normal modules
ansible.builtin.setup:
Embedded/IoT Systems
- name: Manage embedded devices
hosts: iot_devices
gather_facts: false
tasks:
- name: Check system status
ansible.builtin.raw: cat /proc/uptime && free -m && df -h /
register: status
- name: Update firmware
ansible.builtin.raw: /usr/sbin/fw_upgrade /tmp/firmware.bin
when: upgrade_firmware | default(false)
raw vs command vs shell
| Feature | raw | command | shell |
|---|---|---|---|
| Needs Python | ❌ | ✅ | ✅ |
| Pipe/redirect | ✅ (shell-based) | ❌ | ✅ |
creates/removes | ❌ | ✅ | ✅ |
chdir | ❌ | ✅ | ✅ |
changed_when auto | ❌ (always changed) | ✅ | ✅ |
| Environment vars | Via shell syntax | environment: | environment: |
| Use case | No Python, bootstrap | Normal commands | Commands with pipes |
Key Behaviors
| Behavior | Detail |
|---|---|
gather_facts | Must set to false when using raw on Python-less hosts |
| Changed status | Always reports "changed" — use changed_when |
| Return values | stdout, stdout_lines, rc, stderr |
| Idempotency | None — raw doesn't check state |
| Connection | Uses configured connection plugin (SSH, docker, etc.) |
Troubleshooting
| Issue | Solution |
|---|---|
| "Failed to connect" | Check SSH access; raw still needs a connection |
| "module requires Python" | You used a regular module; switch to raw |
gather_facts fails | Set gather_facts: false on the play |
| Command not found | Use full path: /usr/bin/apt-get |
| Always shows "changed" | Add changed_when: "'already installed' in result.stdout" |
| Multi-line commands | Use | block scalar in YAML |
Best Practices
- Only use when necessary — prefer
command/shellwhen Python exists - Always
gather_facts: false— when targeting Python-less systems - Set
changed_when— raw always reports changed - Run
setupafter bootstrap — re-gather facts once Python is installed - Full paths for commands — PATH may not be set on minimal systems
- Use for bootstrap, then switch — raw for Python install, normal modules for everything else
Conclusion
ansible.builtin.raw is Ansible's escape hatch for systems without Python. Use it to bootstrap Python on fresh servers, manage network devices, and interact with embedded systems. But treat it as a stepping stone — install Python with raw, then switch to proper modules for everything else. If you're using raw for anything beyond bootstrapping, you're probably doing it the hard way.