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

ParameterDescriptionExample
cmdCommand string to executecmd: ls -la /tmp
argvCommand as list (safer)argv: [ls, -la, /tmp]
chdirChange to directory firstchdir: /opt/app
createsSkip if file existscreates: /opt/app/installed
removesSkip if file doesn't existremoves: /tmp/lock.pid
stdinPass data to stdinstdin: "yes"
stdin_add_newlineAdd newline to stdinstdin_add_newline: true
strip_empty_endsStrip empty lines from outputstrip_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

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

See 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/src

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

See 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 != 0

command vs shell vs raw

Featurecommandshellraw
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:

CommandUse This Module Instead
mkdiransible.builtin.file (state: directory)
rmansible.builtin.file (state: absent)
chmodansible.builtin.file (mode)
chownansible.builtin.file (owner, group)
cpansible.builtin.copy
ln -sansible.builtin.file (state: link)
systemctlansible.builtin.service or ansible.builtin.systemd
apt installansible.builtin.apt
yum installansible.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.

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home