AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible shell vs command vs raw: When to Use Each Module (Comparison)

By Luca Berton · Published 2024-01-01 · Category: installation

Difference between Ansible shell, command, and raw modules. When to use shell vs command, pipes, redirects, environment variables.

Ansible provides three modules for running commands on remote hosts: command, shell, and raw. Each has different capabilities and security implications. Most of the time, you should use none of them — a purpose-built module is almost always better.

Quick Comparison

Featurecommandshellraw
Shell processingNoYesYes (SSH-level)
Pipes & redirectsNoYesYes
Environment variablesPartialYesDepends on shell
Glob expansionNoYesYes
Python requiredYesYesNo
Idempotent by defaultNoNoNo
changed_when supportYesYesYes
Security riskLowMediumHigh
creates/removesYesYesNo

Module 1: command (Default Choice)

command runs a command directly — no shell processing. This is the safest option.

- name: Check disk space
  ansible.builtin.command: df -h /
  register: disk_space
  changed_when: false

- name: Get hostname
  ansible.builtin.command: hostname -f
  register: fqdn
  changed_when: false

- name: Run a binary with arguments
  ansible.builtin.command:
    cmd: /opt/app/bin/migrate --direction up
    chdir: /opt/app
  register: migration

What command Can't Do

# ❌ FAILS — no pipe support
- ansible.builtin.command: cat /etc/passwd | grep root

# ❌ FAILS — no redirect
- ansible.builtin.command: echo "hello" > /tmp/file.txt

# ❌ FAILS — no glob expansion
- ansible.builtin.command: ls /tmp/*.log

# ❌ FAILS — no environment variable expansion
- ansible.builtin.command: echo $HOME

Why command Is Safer

# With shell, this is dangerous:
- ansible.builtin.shell: "rm -rf {{ user_input }}/"
# If user_input contains "; rm -rf /" → catastrophic

# With command, shell injection is impossible:
- ansible.builtin.command: "rm -rf {{ user_input }}/"
# Arguments are passed directly, no shell interpretation

See also: Ansible for Windows: Complete Guide to Windows Automation (2026)

Module 2: shell (When You Need Shell Features)

shell runs commands through /bin/sh. Use it only when you need pipes, redirects, or shell features.

- name: Find large files (pipe needed)
  ansible.builtin.shell: find /var/log -size +100M | head -20
  register: large_files
  changed_when: false

- name: Redirect output to file
  ansible.builtin.shell: mysqldump mydb > /tmp/backup.sql
  args:
    creates: /tmp/backup.sql

- name: Use environment variable
  ansible.builtin.shell: source /opt/app/.env && /opt/app/bin/start
  args:
    executable: /bin/bash

Making shell Idempotent

# ❌ BAD — always reports changed
- name: Create user
  ansible.builtin.shell: useradd appuser

# ✅ GOOD — creates prevents re-execution
- name: Create user
  ansible.builtin.shell: useradd appuser
  args:
    creates: /home/appuser

# ✅ GOOD — changed_when based on output
- name: Run database migration
  ansible.builtin.shell: /opt/app/bin/migrate up
  register: migrate_result
  changed_when: "'Migrated' in migrate_result.stdout"

# ✅ BEST — use the actual user module
- name: Create user
  ansible.builtin.user:
    name: appuser
    state: present

Module 3: raw (Last Resort)

raw sends commands over SSH without using the Ansible module system. No Python required on the remote host.

# Install Python on a minimal system (bootstrap)
- name: Install Python for Ansible
  ansible.builtin.raw: apt-get update && apt-get install -y python3
  become: true
  changed_when: false

# Network device without Python
- name: Show interface status
  ansible.builtin.raw: show interface brief
  register: interface_status

When raw Is Appropriate

  • Bootstrapping — installing Python on a fresh system
  • Network devices — routers/switches without Python
  • Minimal containers — Alpine/scratch images before Python install
  • Emergency access — when Ansible module subsystem is broken

The Golden Rule: Use Purpose-Built Modules

Before reaching for command/shell/raw, check if a module exists:

# ❌ shell — fragile, not idempotent
- ansible.builtin.shell: apt-get install -y nginx

# ✅ Module — idempotent, cross-platform
- ansible.builtin.apt:
    name: nginx
    state: present

# ❌ shell
- ansible.builtin.shell: systemctl restart nginx

# ✅ Module
- ansible.builtin.systemd:
    name: nginx
    state: restarted

# ❌ shell
- ansible.builtin.shell: cp /tmp/file /etc/file && chmod 644 /etc/file

# ✅ Module
- ansible.builtin.copy:
    src: /tmp/file
    dest: /etc/file
    mode: '0644'
    remote_src: true

# ❌ shell
- ansible.builtin.shell: "echo '{{ line }}' >> /etc/config"

# ✅ Module
- ansible.builtin.lineinfile:
    path: /etc/config
    line: "{{ line }}"

See also: Ansible debug vs assert: When to Use Each Module

Common Module Alternatives

Instead of shell/commandUse this module
apt-get installansible.builtin.apt
yum installansible.builtin.yum / dnf
systemctl start/stopansible.builtin.systemd
cp / mvansible.builtin.copy
echo >> fileansible.builtin.lineinfile
useradd / usermodansible.builtin.user
groupaddansible.builtin.group
chmod / chownansible.builtin.file
mountansible.posix.mount
crontab -eansible.builtin.cron
sysctl -wansible.posix.sysctl
git cloneansible.builtin.git
pip installansible.builtin.pip
docker runcommunity.docker.docker_container
curl / wgetansible.builtin.uri / get_url

ansible-lint Warning: command-instead-of-module

ansible-lint flags unnecessary command/shell usage:

# ansible-lint will warn on this:
- ansible.builtin.command: chown root:root /etc/config
# Suggestion: Use ansible.builtin.file with owner/group parameters

FAQ

When should I use shell instead of command?

Use shell only when you need pipes (|), redirects (>, >>), glob patterns (*.log), environment variable expansion ($HOME), or shell built-ins (source, export). For everything else, command is safer.

Is command or shell idempotent?

Neither is idempotent by default — they always report "changed." Use creates:, removes:, or changed_when: to make them idempotent. Or better, use a purpose-built module that handles idempotency natively.

Why does ansible-lint complain about shell/command?

Because purpose-built modules are idempotent, cross-platform, and handle edge cases (error checking, dry run support). shell/command bypass all of that. ansible-lint encourages you to use the right module for the job.

Can I use bash-specific syntax with shell?

By default, shell uses /bin/sh. For bash features (arrays, [[ ]], process substitution), set executable: /bin/bash:

- ansible.builtin.shell: |
    if [[ -f /etc/config ]]; then
      source /etc/config
    fi
  args:
    executable: /bin/bash

See also: Ansible builtin command Module: Complete Guide with Examples and Best Practices

Conclusion

Use command when you must run a command and no module exists. Use shell only when you need shell features (pipes, redirects). Use raw only for bootstrapping or devices without Python. Always prefer purpose-built modules — they're idempotent, safer, and maintainable.

Ansible shell Module Deep Dive

Running Shell Commands with Pipes and Redirects

- name: Find large files and sort by size
  ansible.builtin.shell: find /var/log -type f -size +100M | sort -rn
  register: large_files

- name: Process pipeline with grep
  ansible.builtin.shell: ps aux | grep nginx | grep -v grep | wc -l
  register: nginx_count
  changed_when: false

- name: Redirect output to file
  ansible.builtin.shell: df -h > /tmp/disk_report.txt 2>&1

Shell with Environment Variables

- name: Run with custom environment
  ansible.builtin.shell: echo $MY_VAR && printenv | grep MY
  environment:
    MY_VAR: "custom_value"
    PATH: "/usr/local/bin:{{ ansible_env.PATH }}"

Shell with Here Documents

- name: Multi-line shell script
  ansible.builtin.shell: |
    #!/bin/bash
    set -euo pipefail
    
    BACKUP_DIR="/backup/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_DIR"
    
    for db in $(mysql -e "SHOW DATABASES" -s --skip-column-names); do
      mysqldump "$db" > "$BACKUP_DIR/$db.sql"
    done
    
    echo "Backup completed: $(ls $BACKUP_DIR | wc -l) databases"
  args:
    executable: /bin/bash
  register: backup_result

Ansible command Module Deep Dive

Command with chdir and creates

- name: Build application (skip if already built)
  ansible.builtin.command:
    cmd: make build
    chdir: /opt/app/src
    creates: /opt/app/bin/myapp
  # Only runs if /opt/app/bin/myapp doesn't exist

Command with removes

- name: Run cleanup (only if temp files exist)
  ansible.builtin.command:
    cmd: /opt/scripts/cleanup.sh
    removes: /tmp/app-cache
  # Only runs if /tmp/app-cache exists

Command with argv (Safe Arguments)

- name: Safe argument passing (no shell injection)
  ansible.builtin.command:
    argv:
      - /usr/bin/git
      - clone
      - --depth
      - "1"
      - "{{ repo_url }}"
      - /opt/app

Security Comparison

Riskcommandshellraw
Shell injection❌ Safe⚠️ Vulnerable❌ Safe
Variable expansionNoYesNo
Pipe/redirect abuseNot possiblePossibleNot possible
Audit trailCleanComplex commands harder to auditMinimal

Shell Injection Example

# DANGEROUS - user input in shell
- ansible.builtin.shell: "grep {{ user_input }} /etc/passwd"
  # If user_input is "; rm -rf /" → disaster

# SAFE - use command module instead
- ansible.builtin.command: "grep {{ user_input }} /etc/passwd"
  # Special characters are treated literally

Performance Tips

Use Pipelining

Enable SSH pipelining to reduce overhead for command/shell tasks:

# ansible.cfg
[ssh_connection]
pipelining = True

Batch Commands

# Slow — 3 SSH connections
- ansible.builtin.command: systemctl stop nginx
- ansible.builtin.command: cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
- ansible.builtin.command: systemctl start nginx

# Faster — 1 SSH connection
- ansible.builtin.shell: |
    systemctl stop nginx
    cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
    systemctl start nginx

# Best — use proper modules (idempotent + no shell)
- ansible.builtin.service:
    name: nginx
    state: stopped
- ansible.builtin.copy:
    src: /etc/nginx/nginx.conf
    dest: /etc/nginx/nginx.conf.bak
    remote_src: true
- ansible.builtin.service:
    name: nginx
    state: started

Ansible raw Module Deep Dive

Bootstrap Python on Remote Host

- name: Install Python on fresh Ubuntu
  ansible.builtin.raw: apt-get install -y python3 python3-apt
  become: true

- name: Install Python on fresh CentOS
  ansible.builtin.raw: yum install -y python3
  become: true

- name: Gather facts after Python is available
  ansible.builtin.setup:

Network Device Commands

- name: Show running config on Cisco device
  ansible.builtin.raw: show running-config
  register: config

- name: Show interface status
  ansible.builtin.raw: show ip interface brief
  register: interfaces

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home