AnsibleIX · command, shell and rawcommand, shell and raw
raw: the escape hatch, and its exact price
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
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.
| Stage | command / shell | raw |
|---|---|---|
| Jinja templating on the controller | yes | yes |
| Module assembled and transferred | yes | no |
| Remote Python interpreter required | yes | no |
| Structured JSON result | yes | no |
| Check-mode support | partial, via creates/removes | none |
environment: keyword applies | yes | only 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
$ ansible-playbook -i localhost, --check checkmode.ymlTASK [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=3The 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
- 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: falseon 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 runsapt-get updateon every host on every run. With it, the play is close to idempotent — as close asrawgets. failed_when: falseon the probe.command -vexits 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:
# Wrong: the host has Python; someone did not want to set the interpreter.
- name: Restart the service
ansible.builtin.raw: systemctl restart appThe 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
argvform 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.
ansible-inventory -i inventories/prod --graph newly_provisioned
ansible-playbook -i inventories/prod bootstrap.yml --list-hostsKnowledge check
Knowledge check · 4 questions
Q1. Why is gather_facts: false mandatory on a play that uses raw to install Python?
Q2. Which guarantees does raw give up compared with ansible.builtin.command? Select all that apply.
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.
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.