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 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 commandAnsible Builtin Command ModuleAnsible error handling

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home