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

Featurerawcommandshell
Needs Python❌✅✅
Pipe/redirect✅ (shell-based)❌✅
creates/removes❌✅✅
chdir❌✅✅
changed_when auto❌ (always changed)✅✅
Environment varsVia shell syntaxenvironment:environment:
Use caseNo Python, bootstrapNormal commandsCommands with pipes

Key Behaviors

BehaviorDetail
gather_factsMust set to false when using raw on Python-less hosts
Changed statusAlways reports "changed" — use changed_when
Return valuesstdout, stdout_lines, rc, stderr
IdempotencyNone — raw doesn't check state
ConnectionUses configured connection plugin (SSH, docker, etc.)

Troubleshooting

IssueSolution
"Failed to connect"Check SSH access; raw still needs a connection
"module requires Python"You used a regular module; switch to raw
gather_facts failsSet gather_facts: false on the play
Command not foundUse full path: /usr/bin/apt-get
Always shows "changed"Add changed_when: "'already installed' in result.stdout"
Multi-line commandsUse | block scalar in YAML

Best Practices

  1. Only use when necessary — prefer command/shell when Python exists
  2. Always gather_facts: false — when targeting Python-less systems
  3. Set changed_when — raw always reports changed
  4. Run setup after bootstrap — re-gather facts once Python is installed
  5. Full paths for commands — PATH may not be set on minimal systems
  6. 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.