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_lines
See 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.lock
See also: Ansible builtin command Module: Complete Guide with Examples and Best Practices
Using 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
- johndoe
Working Directory
- name: Run make in project directory
ansible.builtin.command:
cmd: make install
chdir: /opt/project/src
See also: Ansible shell Module: Run Commands with Pipes & Redirects (Complete Guide)
Environment 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: production
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 != 0
command 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 |
Rule of thumb: Use 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 python3
Common 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: fqdn
Pipeline 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: directory
Common 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: root
Conclusion
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
• Ansible shell vs command • Ansible Builtin Command Module • Ansible error handlingCategory: installation