Skip to main content
RunBook Academy

AnsibleIX · command, shell and rawcommand, shell and raw

raw: the escape hatch, and its exact price

Advanced⏱ ~16 minansible-playbookansible-doc

What you'll learn

  • Describe which stages of the execution pipeline raw bypasses
  • Explain why raw is the only module that works on a host with no Python
  • State what raw gives up: check mode, change reporting and environment
  • Write a bootstrap play correctly, including gather_facts: false
  • Recognise when raw is being used as a shortcut rather than a necessity

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Every other module in this course arrives on the managed node as a Python program. The controller assembles it, ships it, executes it with a remote Python interpreter, and reads back a JSON document describing what happened. That pipeline is where check mode, diff mode, structured results and change detection all live.

ansible.builtin.raw skips all of it. The documentation calls it “a low-down and dirty SSH command, not going through the module subsystem”, which is unusually candid for reference material and exactly accurate.

What raw actually does

raw takes your string and pushes it down the connection to be run by whatever shell the connection gives it. Nothing is assembled, nothing is transferred, nothing is interpreted on the far end except by that shell.

Stagecommand / shellraw
Jinja templating on the controlleryesyes
Module assembled and transferredyesno
Remote Python interpreter requiredyesno
Structured JSON resultyesno
Check-mode supportpartial, via creates/removesnone
environment: keyword appliesyesonly with executable or become

The one row that justifies the module’s existence is the Python one. A freshly provisioned minimal image, a network appliance, a rescue environment, a container built FROM scratch — none of them can run an Ansible module, because there is no interpreter to run it with. raw works because it needs nothing but a shell on the other end of the connection.

The three things it gives up

1. Check mode: none at all

Read-only / Saferaw under --check
$ ansible-playbook -i localhost, --check checkmode.yml
TASK [command guarded by creates] **********************************************
ok: [localhost]

TASK [shell with no guard] *****************************************************
skipping: [localhost]

TASK [raw] *********************************************************************
skipping: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=3

The difference from command matters. command and shell declare check-mode support as partial, and the partial part is creates and removes. raw declares support: none and has neither option, so there is no guard you can add. A raw task can never participate in a dry run.

2. Change reporting: there is none to have

ansible-doc states it in one line: “There is no change handler support for this module.” A raw task reports changed when it ran, which is every time. changed_when still works, because that is evaluated by the task engine on the controller rather than by the module — but you are deriving it from stdout and a return code, with no structured result to work from.

3. environment: mostly does not apply

The documented behaviour: “The environment keyword does not work with raw normally, it requires a shell which means it only works if executable is set or using the module with privilege escalation (become).”

This is the one that produces a genuinely baffling bug report. You set environment: { http_proxy: 'http://proxy.example.com:3128' } on a bootstrap play, the package install still cannot reach the internet, and nothing in the output mentions the proxy at all.

The legitimate use: bootstrapping Python

Configuration changethe bootstrap play
- name: Bootstrap an interpreter onto a bare host
hosts: newly_provisioned
gather_facts: false            # mandatory: fact gathering needs Python
become: true
tasks:
  - name: Is there already an interpreter?
    ansible.builtin.raw: command -v python3
    register: python_probe
    failed_when: false
    changed_when: false

  - name: Install Python on Debian-family hosts
    ansible.builtin.raw: apt-get update && apt-get install -y python3
    when: python_probe.rc != 0
    changed_when: python_probe.rc != 0

- name: Everything from here is ordinary Ansible
hosts: newly_provisioned
gather_facts: true
become: true
tasks:
  - name: Confirm the interpreter Ansible chose
    ansible.builtin.debug:
      var: ansible_facts['python']['executable']

Four details in that play are not decoration:

  • gather_facts: false on the first play. The setup module is a Python module. On a host with no Python it fails before your first task runs, and the error names the interpreter rather than the problem. This is the single most common bootstrap mistake.
  • A probe first, and when: on the install. Without it the task runs apt-get update on every host on every run. With it, the play is close to idempotent — as close as raw gets.
  • failed_when: false on the probe. command -v exits non-zero when the binary is absent, which is the answer you asked for, not a failure.
  • A second play. The moment Python exists, stop using raw. The second play gathers facts and behaves normally.

When raw is being used as a shortcut

There are two other uses that are defensible and one that is not.

Defensible: devices that will never have Python — some switches, some appliances, some embedded systems. And rescue situations, where the host is broken in a way that stops modules working and you need a transport that requires nothing.

Not defensible: raw chosen because someone hit an interpreter-discovery warning and this made it go away. The symptom looks like:

Service impact possibleraw as a workaround
# Wrong: the host has Python; someone did not want to set the interpreter.
- name: Restart the service
ansible.builtin.raw: systemctl restart app

The correct fix is ansible_python_interpreter in inventory, or letting discovery work. The interpreter-discovery part of this course covers both. raw as an interpreter workaround trades away the entire module subsystem to avoid setting one variable.

Blast radius

A raw task is a shell command running on every targeted host, usually as root (bootstrap plays almost always carry become: true), with:

  • no dry run available at all,
  • no structured result to reason about afterwards,
  • and no argv form to defend against an interpolated variable.

That combination makes raw the highest-risk module in the course. The mitigation is scope: a bootstrap play should target a group that contains only hosts you have just provisioned, and that group should be built from something authoritative rather than from a pattern.

Read-only / Safeconfirm the group is what you think
ansible-inventory -i inventories/prod --graph newly_provisioned
ansible-playbook -i inventories/prod bootstrap.yml --list-hosts

Knowledge check

Knowledge check · 4 questions

  1. Q1. Why is gather_facts: false mandatory on a play that uses raw to install Python?

  2. Q2. Which guarantees does raw give up compared with ansible.builtin.command? Select all that apply.

  3. Q3. raw is the only execution module that works on a managed node with no Python interpreter at all, because the command is passed directly into the connection string.

  4. Q4. A colleague replaces ansible.builtin.systemd with raw: systemctl restart app because an interpreter-discovery warning was appearing. What is the strongest objection?

Passing score: 75%. Answers are checked in this browser.