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

| Feature | command | shell | raw | |---------|---------|-------|-----| | Shell processing | No | Yes | Yes (SSH-level) | | Pipes & redirects | No | Yes | Yes | | Environment variables | Partial | Yes | Depends on shell | | Glob expansion | No | Yes | Yes | | Python required | Yes | Yes | No | | Idempotent by default | No | No | No | | changed_when support | Yes | Yes | Yes | | Security risk | Low | Medium | High | | creates/removes | Yes | Yes | No |

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

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

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

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

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 builtin command Module: Complete Guide with Examples and Best Practices

Common Module Alternatives

| Instead of shell/command | Use this module | |------------------------|----------------| | apt-get install | ansible.builtin.apt | | yum install | ansible.builtin.yum / dnf | | systemctl start/stop | ansible.builtin.systemd | | cp / mv | ansible.builtin.copy | | echo >> file | ansible.builtin.lineinfile | | useradd / usermod | ansible.builtin.user | | groupadd | ansible.builtin.group | | chmod / chown | ansible.builtin.file | | mount | ansible.posix.mount | | crontab -e | ansible.builtin.cron | | sysctl -w | ansible.posix.sysctl | | git clone | ansible.builtin.git | | pip install | ansible.builtin.pip | | docker run | community.docker.docker_container | | curl / wget | ansible.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

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.

Related Articles

Ansible copy vs template vs lineinfileAnsible changed_when / failed_when GuideAnsible Lint: Analyze & Fix PlaybooksAnsible Check Mode / Dry Run Guide

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

| Risk | command | shell | raw | |------|---------|-------|-----| | Shell injection | ❌ Safe | ⚠️ Vulnerable | ❌ Safe | | Variable expansion | No | Yes | No | | Pipe/redirect abuse | Not possible | Possible | Not possible | | Audit trail | Clean | Complex commands harder to audit | Minimal |

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