Ansible Quickstart — For Dummies

Zero jargon. In ten minutes you will have Ansible installed, a real playbook running against a real host, and a gut-level feel for what it does.

What Ansible is, in three sentences

Ansible is a tool you run on your laptop that SSHes into other machines and makes them match a description you wrote in a YAML file. You describe the end state ("nginx is installed, this config file is in place, the service is running"), and Ansible figures out what to change to get there. It runs the same playbook against 1 server or 1000 servers, and running it a second time does nothing if nothing needs changing.

That's it. No agents on the remote hosts, no daemon, no database. Just SSH and Python.

Install it on your laptop

You only install it on the machine you'll run commands from (your laptop, a jump host, a CI runner). The remote machines need nothing beyond SSH and a Python interpreter — which already exists on every Linux server.

# On macOS
brew install ansible

# On Debian / Ubuntu
sudo apt install ansible

# On RHEL / Rocky / Fedora
sudo dnf install ansible

# Or the universal way — isolated Python install
python3 -m pip install --user pipx
pipx install ansible

# Check it works
ansible --version

pipx is the recommended path if you want the latest version and don't want to fight your distro's package manager.

The three things you'll touch

  1. An inventory file — a plain-text list of hosts to manage.
  2. A playbook — a YAML file describing what you want those hosts to look like.
  3. The ansible-playbook command — runs a playbook against an inventory.

That's the entire product surface for the first week. Everything else (roles, collections, vaults, variables) is an organisational convenience on top of these three things.

Write an inventory

Make a new directory and put a file called hosts.ini in it with one host you can SSH into.

mkdir ~/ansible-first
cd ~/ansible-first

cat > hosts.ini <<'EOF'
[dev]
lab01.example.com ansible_user=ubuntu
EOF

That file says: there is a group called dev with one host, and Ansible should SSH in as the user ubuntu. Replace with a hostname you actually have, and a user you can SSH to with key auth.

If you can run ssh lab01.example.com and land in a shell without typing a password, Ansible can do its thing. If you can't, fix SSH first — see SSH Keys.

Ad-hoc: your first command

Before writing a playbook, just prove the connection works. An "ad-hoc" command runs a single module against the inventory.

# Say hello — the ping module checks SSH + Python on the remote
ansible -i hosts.ini all -m ping

# Expected output:
# lab01.example.com | SUCCESS => {
#     "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" },
#     "changed": false,
#     "ping": "pong"
# }

If that printed SUCCESS, you're done with setup. Everything else is just writing YAML.

A couple more useful ad-hoc commands while you have the momentum:

# Run a shell command on every host in the group
ansible -i hosts.ini dev -m shell -a 'uptime'

# Gather and print facts (tons of info about the remote host)
ansible -i hosts.ini dev -m setup | head -40

Your first playbook

Create a file called first.yml:

---
- name: My very first playbook
  hosts: dev
  become: true

  tasks:
    - name: Make sure htop is installed
      ansible.builtin.package:
        name: htop
        state: present

    - name: Write a friendly message of the day
      ansible.builtin.copy:
        dest: /etc/motd
        content: "Managed by Ansible. Do not hand-edit.\n"
        owner: root
        group: root
        mode: '0644'

Run it:

ansible-playbook -i hosts.ini first.yml

Expected output (trimmed):

PLAY [My very first playbook] ***************************************

TASK [Gathering Facts] **********************************************
ok: [lab01.example.com]

TASK [Make sure htop is installed] **********************************
changed: [lab01.example.com]

TASK [Write a friendly message of the day] **************************
changed: [lab01.example.com]

PLAY RECAP **********************************************************
lab01.example.com : ok=3   changed=2   unreachable=0   failed=0

changed=2 means Ansible actually did something — htop wasn't installed before, motd wasn't correct. SSH in and check: htop runs, cat /etc/motd shows your message.

What just happened, line by line

---
- name: My very first playbook    # human-readable play name (prints in output)
  hosts: dev                      # which inventory group to target
  become: true                    # run tasks as root (via sudo on the remote)

  tasks:                          # ordered list of things to do
    - name: Make sure htop is installed
      ansible.builtin.package:    # the "package" module — distro-agnostic
        name: htop
        state: present            # desired state: must exist

    - name: Write a friendly message of the day
      ansible.builtin.copy:       # the "copy" module — writes files
        dest: /etc/motd           # where the file goes on the remote
        content: "..."            # the literal content (instead of src:)
        owner: root               # file ownership
        group: root
        mode: '0644'              # permissions

Every task has a human name, a module name (ansible.builtin.package, ansible.builtin.copy), and a set of arguments. The module does the actual work — Ansible is mostly just shuttling arguments to the right module on the right host.

The two modules above (package and copy) plus service, file, lineinfile, user, template, and command cover the first year of real-world usage.

Run it again — idempotency

Without changing anything, run the same command again:

ansible-playbook -i hosts.ini first.yml
PLAY RECAP **********************************************************
lab01.example.com : ok=3   changed=0   unreachable=0   failed=0

changed=0. htop was already installed; motd already had the right content. Ansible saw that the desired state was already met and did nothing.

This is called idempotency and it is the single most important idea in the tool. You are not writing a script of actions. You are declaring a state. Ansible only acts when the actual state doesn't match.

Variables, the short version

Hard-coding htop in a task is fine for one box. For a real fleet you'll want variables. Three places they come from, in order of increasing specificity:

  1. Inline in the playbook — quick and dirty.
  2. group_vars/<group>.yml — applies to every host in that group.
  3. host_vars/<hostname>.yml — applies to one specific host (overrides group).

Example structure:

ansible-first/
├── hosts.ini
├── first.yml
├── group_vars/
│   └── dev.yml           # variables for every host in the [dev] group
└── host_vars/
    └── lab01.example.com.yml   # just for this one host

group_vars/dev.yml:

---
motd_message: "Dev environment — reboots at 03:00 UTC daily."
packages_to_install:
  - htop
  - jq
  - curl

Updated first.yml that uses those variables:

---
- name: My very first playbook (with variables)
  hosts: dev
  become: true

  tasks:
    - name: Install baseline packages
      ansible.builtin.package:
        name: "{{ packages_to_install }}"
        state: present

    - name: Write motd
      ansible.builtin.copy:
        dest: /etc/motd
        content: "{{ motd_message }}\n"
        owner: root
        group: root
        mode: '0644'

The {{ ... }} is Jinja2. Don't worry about it yet — just know curly braces mean "substitute a variable here".

Handlers in one paragraph

When you change a config file you usually want to restart a service — but only if something actually changed. That's a handler: a task that only runs when another task notifys it.

---
- name: Configure nginx
  hosts: web
  become: true

  tasks:
    - name: Deploy nginx config
      ansible.builtin.copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf
        mode: '0644'
      notify: Restart nginx

  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

First run: config changes, handler fires, nginx restarts. Second run: config already correct, handler doesn't fire. Zero unnecessary restarts. See Handlers & Templates for the full picture.

Where to go next

You now understand what Ansible is and how to use it at the simplest level. The other pages on this site build on exactly this foundation — none of them introduce a new fundamental concept, they just show you how real production repos are organised.

This is the smallest useful thing. It's deliberately not how you'd organise a production repo — but it's exactly enough to start automating your own hosts tomorrow. Read the other pages when you hit a limit.