--- title: "Ansible Getting Started: Inventory, Playbooks, and Ad-Hoc Commands" domain: linux category: shell-scripting tags: [ansible, automation, infrastructure, linux, idempotent] status: published created: 2026-03-08 updated: 2026-03-08 --- # 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 ```bash # 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 ```ini # 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: ```bash 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: ```bash # 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 ```yaml --- # 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: ```bash 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": ```yaml 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: ```yaml - 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](../process-management/managing-linux-services-systemd-ansible.md) - [linux-server-hardening-checklist](../../02-selfhosting/security/linux-server-hardening-checklist.md)