Skip to content

Ansible Getting Started: Inventory, Playbooks, and Ad-Hoc Commands

Ansible is how I manage infrastructure at scale — or even just across a handful of machines. You write what you want the end state to look like, Ansible figures out how to get there. No agents needed on the remote machines, just SSH.

The Short Answer

# Install Ansible
pip install ansible

# Run a one-off command on all hosts
ansible all -i inventory.ini -m ping

# Run a playbook
ansible-playbook -i inventory.ini site.yml

Core Concepts

Inventory — the list of machines Ansible manages. Can be a static file or dynamically generated.

Playbook — a YAML file describing tasks to run on hosts. The main thing you write.

Module — the building blocks of tasks. apt, dnf, service, copy, template, user, etc. Ansible has modules for almost everything.

Idempotency — run the same playbook ten times, the result is the same as running it once. Ansible modules are designed this way. This matters because it means you can re-run playbooks safely without side effects.

Inventory File

# inventory.ini

[webservers]
web1.example.com
web2.example.com ansible_user=admin

[databases]
db1.example.com ansible_user=ubuntu ansible_port=2222

[all:vars]
ansible_user=myuser
ansible_ssh_private_key_file=~/.ssh/id_ed25519

Test connectivity:

ansible all -i inventory.ini -m ping

A successful response looks like:

web1.example.com | SUCCESS => {
    "ping": "pong"
}

Ad-Hoc Commands

For quick one-offs without writing a playbook:

# Run a shell command
ansible all -i inventory.ini -m shell -a "uptime"

# Install a package
ansible webservers -i inventory.ini -m apt -a "name=nginx state=present" --become

# Restart a service
ansible webservers -i inventory.ini -m service -a "name=nginx state=restarted" --become

# Copy a file
ansible all -i inventory.ini -m copy -a "src=./myfile dest=/tmp/myfile"

--become escalates to sudo.

Writing a Playbook

---
# site.yml
- name: Configure web servers
  hosts: webservers
  become: true

  vars:
    app_port: 8080

  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present

    - name: Start and enable nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

    - name: Deploy config file
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: Reload nginx

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Run it:

ansible-playbook -i inventory.ini site.yml

# Dry run — shows what would change without doing it
ansible-playbook -i inventory.ini site.yml --check

# Verbose output
ansible-playbook -i inventory.ini site.yml -v

Handlers

Handlers run at the end of a play, only if notified. The canonical use is "reload service after config change":

tasks:
  - name: Deploy config
    ansible.builtin.template:
      src: templates/app.conf.j2
      dest: /etc/app/app.conf
    notify: Restart app

handlers:
  - name: Restart app
    ansible.builtin.service:
      name: myapp
      state: restarted

If the config file didn't change (idempotent — it was already in the right state), the notify never fires and the service isn't restarted.

Roles

Once playbooks get complex, organize them into roles:

roles/
  webserver/
    tasks/
      main.yml
    handlers/
      main.yml
    templates/
      nginx.conf.j2
    defaults/
      main.yml

Use a role in a playbook:

- name: Set up web servers
  hosts: webservers
  become: true
  roles:
    - webserver

Roles keep things organized and reusable across projects.

Gotchas & Notes

  • YAML indentation matters. Two spaces is standard. Tab characters will break your playbooks.
  • --check is your friend. Always dry-run against production before applying changes.
  • SSH key access is required. Ansible connects over SSH — password auth works but key auth is what you want for automation.
  • gather_facts: false speeds up playbooks when you don't need host facts (OS, IP, etc.). Add it at the play level for simple playbooks.
  • Ansible is not idempotent by magic. Shell and command modules run every time regardless of state. Use the appropriate module (apt, service, file, etc.) instead of shell whenever possible.
  • The ansible-lint tool catches common mistakes before they run. Worth adding to your workflow.

See Also

  • [[managing-linux-services-systemd-ansible]]
  • [[linux-server-hardening-checklist]]