Ansible command Module: Run Shell Commands on Remote Hosts (ansible.builtin.command)
By Luca Berton · Published 2024-01-01 · Category: installation
How to run commands on remote hosts with Ansible command module (ansible.builtin.command). Execute binaries, use creates/removes guards.
The ansible.builtin.command module runs commands on remote hosts without shell processing. This means no pipes, redirects, or variable expansion — making it more secure than shell for most tasks. Here's everything you need to know.
Basic Usage
- name: Check disk space
ansible.builtin.command: df -h
register: disk_output
- name: Show output
ansible.builtin.debug:
var: disk_output.stdout_linesSee also: Run a SQL Command/Query on PostgreSQL - Ansible module postgresql_query
Key Parameters
| Parameter | Description | Example |
|---|---|---|
cmd | Command string to execute | cmd: ls -la /tmp |
argv | Command as list (safer) | argv: [ls, -la, /tmp] |
chdir | Change to directory first | chdir: /opt/app |
creates | Skip if file exists | creates: /opt/app/installed |
removes | Skip if file doesn't exist | removes: /tmp/lock.pid |
stdin | Pass data to stdin | stdin: "yes" |
stdin_add_newline | Add newline to stdin | stdin_add_newline: true |
strip_empty_ends | Strip empty lines from output | strip_empty_ends: true |
Idempotent Command Execution
The biggest problem with command is it always reports changed. Use creates and removes for idempotency:
# Only runs if /opt/app doesn't exist
- name: Install application
ansible.builtin.command:
cmd: /tmp/installer.sh
creates: /opt/app
# Only runs if lock file exists
- name: Clean up stale lock
ansible.builtin.command:
cmd: rm /tmp/app.lock
removes: /tmp/app.lockUsing argv for Safe Arguments
When arguments contain spaces or special characters, use argv:
- name: Create user with spaces in comment
ansible.builtin.command:
argv:
- useradd
- -c
- "John Doe - Engineering Team"
- -m
- johndoeSee also: Ansible builtin command Module: Complete Guide with Examples and Best Practices
Working Directory
- name: Run make in project directory
ansible.builtin.command:
cmd: make install
chdir: /opt/project/srcEnvironment Variables
- name: Run with custom environment
ansible.builtin.command:
cmd: ./build.sh
chdir: /opt/project
environment:
PATH: "/opt/custom/bin:{{ ansible_env.PATH }}"
JAVA_HOME: /usr/lib/jvm/java-17
BUILD_ENV: productionSee also: Ansible shell Module: Run Commands with Pipes & Redirects (Complete Guide)
Capturing Output
- name: Get application version
ansible.builtin.command: /opt/app/bin/app --version
register: app_version
changed_when: false
- name: Display version
ansible.builtin.debug:
msg: "App version: {{ app_version.stdout }}"
- name: Fail if wrong version
ansible.builtin.fail:
msg: "Expected v2.0, got {{ app_version.stdout }}"
when: "'v2.0' not in app_version.stdout"Handling Return Codes
# Treat specific return codes as success
- name: Check service (rc=3 means not running, which is OK)
ansible.builtin.command: systemctl is-active myservice
register: service_check
failed_when: service_check.rc not in [0, 3]
changed_when: false
# Ignore errors and handle manually
- name: Try to get config
ansible.builtin.command: cat /etc/myapp/config.yml
register: config_result
ignore_errors: true
- name: Create default config if missing
ansible.builtin.copy:
content: "default: true\n"
dest: /etc/myapp/config.yml
when: config_result.rc != 0command vs shell vs raw
| Feature | command | shell | raw | |
|---|---|---|---|---|
| Shell processing | ❌ No | ✅ Yes | ✅ Yes | |
| Pipes & redirects | ❌ | ✅ \ | , >, >> | ✅ |
| Variable expansion | ❌ | ✅ $HOME | ✅ | |
| Glob patterns | ❌ | ✅ .log | ✅ | |
| Python required | ✅ | ✅ | ❌ | |
| Security | 🟢 Safest | 🟡 Injection risk | 🔴 Most risk |
command unless you specifically need shell features. Use shell for pipes and redirects. Use raw only for hosts without Python.
# command — safe, no shell
- ansible.builtin.command: ls -la /tmp
# shell — needed for pipe
- ansible.builtin.shell: ps aux | grep nginx | wc -l
# raw — no Python required (e.g., network devices, bootstrap)
- ansible.builtin.raw: apt-get install -y python3Common Patterns
Check Before Acting
- name: Check if migrations needed
ansible.builtin.command:
cmd: python manage.py showmigrations --plan
chdir: /opt/webapp
register: migrations
changed_when: false
- name: Run migrations
ansible.builtin.command:
cmd: python manage.py migrate --noinput
chdir: /opt/webapp
when: "'[ ]' in migrations.stdout"Suppress Changed Status
# Read-only commands should never report "changed"
- name: Get hostname
ansible.builtin.command: hostname -f
changed_when: false
register: fqdnPipeline Alternative Without Shell
# Instead of: shell: "grep ERROR /var/log/app.log | wc -l"
# Use command + register + filter:
- name: Get log contents
ansible.builtin.command: cat /var/log/app.log
register: log_content
changed_when: false
- name: Count errors
ansible.builtin.set_fact:
error_count: "{{ log_content.stdout_lines | select('search', 'ERROR') | list | length }}"Warnings and Ansible-Lint
Ansible-lint flags command when a built-in module exists:
# BAD — ansible-lint warns: Use ansible.builtin.file instead
- ansible.builtin.command: mkdir -p /opt/app
# GOOD — use the native module
- ansible.builtin.file:
path: /opt/app
state: directoryCommon replacements:
| Command | Use This Module Instead |
|---|---|
mkdir | ansible.builtin.file (state: directory) |
rm | ansible.builtin.file (state: absent) |
chmod | ansible.builtin.file (mode) |
chown | ansible.builtin.file (owner, group) |
cp | ansible.builtin.copy |
ln -s | ansible.builtin.file (state: link) |
systemctl | ansible.builtin.service or ansible.builtin.systemd |
apt install | ansible.builtin.apt |
yum install | ansible.builtin.yum or ansible.builtin.dnf |
FAQ
When should I use command instead of shell?
Always prefer command unless you need shell features like pipes (|), redirects (>), variable expansion ($VAR), or glob patterns (.log). The command module is more secure because it doesn't pass through a shell interpreter, eliminating shell injection risks.
How do I make command idempotent?
Use the creates parameter to skip execution when a file already exists, or removes to skip when a file doesn't exist. For more complex idempotency, use register + changed_when with a conditional check.
Why does command always show "changed"?
The command module has no way to know if the command actually changed anything. Use changed_when: false for read-only commands, or changed_when with a condition based on the registered output.
Can I use command with sudo?
Yes, use Ansible's become mechanism instead of putting sudo in the command:
- name: Run as root
ansible.builtin.command: whoami
become: true
become_user: rootConclusion
Use ansible.builtin.command as your default for running commands on remote hosts. It's safer than shell because it skips shell processing. Make your commands idempotent with creates/removes, suppress false changed results with changed_when: false, and consider native modules before reaching for command.
Related Articles
Category: installation