Ad-hoc Commands and Playbooks

Last updated: June 29, 2025

From Quick Tasks to Orchestrated Automation

When I first started with Ansible, I found myself in a situation where I needed to check disk space on 50+ servers immediately. I didn't have time to write a script, and logging into each server individually would have taken hours. That's when I discovered the power of Ansible ad-hoc commands. With a single line, I was able to check disk usage across all my servers in seconds. Later, as my automation needs grew more complex, I graduated to playbooks for repeatable, documented automation. In this post, I'll share my journey with both these powerful Ansible features.

Ad-hoc Commands: The Command-Line Magic

Ad-hoc commands are Ansible's way of providing command-line magic for one-off tasks. They're perfect for when you need to do something quick without the overhead of creating a playbook. Think of them as the Swiss Army knife in your Ansible toolkit.

The Basic Syntax

The basic syntax of an ad-hoc command is:

ansible [pattern] -m [module] -a "[module options]"

Where:

  • [pattern] is the host or group you're targeting

  • -m [module] specifies which module to use (defaults to command if omitted)

  • -a "[module options]" provides arguments to the module

When to Use Ad-hoc Commands

I've found ad-hoc commands invaluable for:

  1. Quick checks: Verifying system status across multiple servers

  2. Simple changes: Making the same change on multiple systems

  3. Emergency fixes: Addressing urgent issues quickly

  4. Fact gathering: Collecting information about your infrastructure

  5. Testing: Confirming connectivity or module behavior before adding to playbooks

Real-world Examples for Linux

Checking System Status

One of the first things I do when troubleshooting is check system load across all affected servers:

ansible webservers -m ansible.builtin.shell -a "uptime"

Or check disk space usage:

ansible all -m ansible.builtin.shell -a "df -h"

Running Commands with Privilege Escalation

For tasks that require root access:

ansible webservers -m ansible.builtin.shell -a "tail /var/log/secure" --become

The --become flag (or -b for short) makes Ansible use privilege escalation, running the command as root.

Managing Packages Quickly

Need to install a package on all database servers? No problem:

ansible dbservers -m ansible.builtin.yum -a "name=postgresql state=latest" --become

Or remove a vulnerable package from everywhere:

ansible all -m ansible.builtin.apt -a "name=vulnerable-package state=absent" --become

Real-world Examples for Windows

Ad-hoc commands work just as well for Windows systems when you have the right setup:

Checking Windows Services

ansible windows_servers -m ansible.windows.win_service -a "name=Spooler"

Installing Windows Updates

ansible windows_servers -m ansible.windows.win_updates -a "category_names=['SecurityUpdates', 'CriticalUpdates']"

Checking Disk Space on Windows

ansible windows_servers -m ansible.windows.win_shell -a "Get-PSDrive C | Select-Object Used,Free"

Playbooks: Automation as Code

While ad-hoc commands are great for one-off tasks, I quickly found myself repeating certain sequences of operations. This is where Ansible playbooks shine. Playbooks are like automation scripts on steroids – they define a set of tasks to be executed in sequence, with powerful features like variables, conditionals, and loops.

Anatomy of a Playbook

Playbooks are written in YAML format. Here's the basic structure:

---
- name: Play name
  hosts: target_hosts
  become: yes  # If you need privilege escalation

  tasks:
    - name: First task description
      module_name:
        option1: value1
        option2: value2

    - name: Second task description
      another_module:
        option: value

The Power of Idempotence

One thing I love about Ansible is its idempotent nature. This means you can run a playbook multiple times, and after the first run, no changes will be made unless they're needed. This gives me confidence to run playbooks regularly to ensure systems stay in the desired state.

My First Multi-OS Playbook Journey

Early in my Ansible journey, I needed to create a playbook that would work across both Linux and Windows servers. Here's a simplified version of what I created:

---
- name: Configure common settings across all servers
  hosts: all
  become: yes  # Will be ignored on Windows if using SSH

  tasks:
    # Task for both Linux and Windows
    - name: Ensure NTP is running
      block:
        - name: Configure NTP on Linux
          ansible.builtin.service:
            name: "{{ 'chronyd' if ansible_os_family == 'RedHat' else 'ntp' }}"
            state: started
            enabled: yes
          when: ansible_os_family != "Windows"

        - name: Configure NTP on Windows
          ansible.windows.win_service:
            name: W32Time
            state: started
            start_mode: auto
          when: ansible_os_family == "Windows"

    # Security updates
    - name: Apply security updates
      block:
        - name: Apply Linux security updates
          ansible.builtin.package:
            name: "*"
            state: latest
            security: yes
          when: ansible_os_family != "Windows"
          
        - name: Apply Windows security updates  
          ansible.windows.win_updates:
            category_names:
              - SecurityUpdates
            reboot: no
          when: ansible_os_family == "Windows"

This playbook taught me the value of using conditionals based on the operating system (ansible_os_family) to apply the right tasks to the right systems.

Sequence Diagram: Ad-hoc Command vs. Playbook Execution

To better understand how ad-hoc commands and playbooks work under the hood, let me share a sequence diagram that compares their execution flows:

The key differences illustrated here are:

  1. Ad-hoc commands execute a single module once

  2. Playbooks organize multiple tasks with logic and structure

  3. Playbook execution includes variable processing and handler management

Best Practices I've Learned

After years of using Ansible, I've developed some best practices that have saved me countless hours:

For Ad-hoc Commands

  1. Use --limit for testing: When trying a new command, limit it to one server first.

    ansible all -m ping --limit test-server-01
  2. Leverage --check mode: See what would change without making actual changes.

    ansible webservers -m yum -a "name=httpd state=latest" --check
  3. Increase parallelism for large environments: Use -f to set the number of parallel processes.

    ansible all -m ping -f 50
  4. Save complex ad-hoc commands: If you find yourself using the same ad-hoc command regularly, it's time for a playbook.

For Playbooks

  1. Start simple, grow incrementally: Begin with a basic playbook and add complexity as needed.

  2. Use roles for organization: As your playbooks grow, refactor common tasks into roles for reusability.

  3. Always test playbooks in dev: Never run a new playbook against production servers first.

  4. Use variables for environment-specific values: This keeps your playbooks flexible.

    # group_vars/production.yml
    ntp_server: prod-ntp.example.com
    
    # group_vars/development.yml
    ntp_server: dev-ntp.example.com
    
    # In your playbook
    - name: Set NTP server
      template:
        src: ntp.conf.j2
        dest: /etc/ntp.conf
  5. Include helpful comments: Your future self will thank you.

    - name: Configure database settings 
      # This must run after the schema initialization
      # See PROJ-1234 for details on the required settings
      template:
        src: db_config.j2
        dest: /etc/app/database.conf

Real-world Playbook Examples

Linux Server Hardening

Here's a simplified version of a security hardening playbook I've used:

---
- name: Basic Linux hardening
  hosts: linux_servers
  become: yes
  
  tasks:
    - name: Ensure key security packages are installed
      package:
        name:
          - fail2ban
          - auditd
          - rkhunter
        state: present
    
    - name: Configure SSH to disable root login
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^PermitRootLogin'
        line: 'PermitRootLogin no'
      notify: Restart SSH
    
    - name: Set strong password policies
      blockinfile:
        path: /etc/security/pwquality.conf
        block: |
          minlen = 12
          dcredit = -1
          ucredit = -1
          ocredit = -1
          lcredit = -1
          
  handlers:
    - name: Restart SSH
      service:
        name: sshd
        state: restarted

Windows Server Setup

And here's a playbook for setting up IIS on Windows servers:

---
- name: Configure IIS Web Servers
  hosts: windows_web_servers
  
  tasks:
    - name: Install IIS
      win_feature:
        name: Web-Server
        state: present
        include_management_tools: yes
      register: iis_install
      
    - name: Reboot if required
      win_reboot:
      when: iis_install.reboot_required
      
    - name: Create website directory
      win_file:
        path: C:\websites\{{ website_name }}
        state: directory
      
    - name: Deploy website content
      win_copy:
        src: files/website/
        dest: C:\websites\{{ website_name }}
        
    - name: Configure new website
      win_iis_website:
        name: "{{ website_name }}"
        state: started
        port: 80
        physical_path: C:\websites\{{ website_name }}
        application_pool: "{{ website_name }}_pool"

Migrating from Ad-hoc to Playbooks

As your automation needs grow, you'll likely start with ad-hoc commands and gradually move to playbooks. Here's the process I follow:

  1. Identify repetitive ad-hoc commands: If you're running the same ad-hoc command regularly, it's a candidate for a playbook.

  2. Document the steps: Write down what you're trying to accomplish.

  3. Create a simple playbook: Start with a basic version that does what your ad-hoc command did.

  4. Test and expand: Test your playbook, then gradually add error handling, variables, and conditionals.

  5. Refactor into roles: As your playbook grows, consider splitting it into reusable roles.

For example, I started with this ad-hoc command to check disk space:

ansible webservers -m ansible.builtin.shell -a "df -h | grep -v tmpfs"

Which evolved into this playbook for monitoring and alerting:

---
- name: Check disk space and alert if needed
  hosts: all
  gather_facts: yes
  
  tasks:
    - name: Get disk usage
      ansible.builtin.shell: df -h / | tail -n 1 | awk '{print $5}' | sed 's/%//'
      register: disk_usage
      changed_when: false
      
    - name: Alert if disk usage is high
      debug:
        msg: "WARNING: Disk usage on {{ inventory_hostname }} is {{ disk_usage.stdout }}%"
      when: disk_usage.stdout|int > 90
      
    - name: Send alert email
      mail:
        to: "{{ alert_email }}"
        subject: "High disk usage alert for {{ inventory_hostname }}"
        body: "Disk usage is at {{ disk_usage.stdout }}% on {{ inventory_hostname }}"
      when: disk_usage.stdout|int > 90

Conclusion: The Right Tool for the Job

After years of working with Ansible, I've come to appreciate having both ad-hoc commands and playbooks in my toolbox. They each have their place:

  • Ad-hoc commands are perfect for quick, one-time tasks, troubleshooting, and information gathering.

  • Playbooks shine for repeatable processes, complex orchestration, and documented automation.

The best Ansible users know when to use each tool. Start with ad-hoc commands for simplicity, then graduate to playbooks as your automation needs mature. Together, they form a powerful combination that can tackle virtually any automation challenge across both Linux and Windows environments.

The journey from running a simple command on multiple servers to orchestrating complex, multi-tier application deployments becomes much more manageable when you master these two fundamental aspects of Ansible.


What's your favorite ad-hoc command or playbook trick? I'd love to hear your experiences in the comments below!

Last updated