TechEarl

Ansible: Install and Write Your First Playbook

Ansible getting started: install it, write an inventory and your first playbook, and run it against a remote host over SSH. No agent, no daemon, just Python and a YAML file.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
Ansible getting started guide: install Ansible, write an inventory file and your first playbook, and run it over SSH against a remote Linux host. No agent required.

Three files and one command get you a working Ansible setup: install Ansible on your control machine, write a one-line inventory.ini listing the host you want to manage, write a short playbook.yml, then run it:

bash
ansible-playbook -i inventory.ini playbook.yml

Ansible getting started is genuinely this short because Ansible is agentless. There is nothing to install on the machines you manage. The control node talks to them over plain SSH and runs short-lived Python, so a fresh Linux box with an SSH key already on it is ready to be configured with no prep. This is the whole loop, from install to a playbook that actually changes a remote host.

Install Ansible on the control node

You only install Ansible on the one machine you drive everything from (your laptop, a CI runner, a jump box). The managed hosts get nothing.

The cleanest install is pipx, which gives you a current Ansible in its own isolated virtualenv. This is the route the Ansible docs recommend, and it sidesteps the externally-managed-environment error you now hit if you try a plain pip install on Debian 12+, Ubuntu 23.04+, Fedora, or Homebrew Python (PEP 668 marks the system Python as off-limits to pip):

bash
pipx install --include-deps ansible

Or use the package manager if you prefer it pinned to the distro (older, but zero setup):

bash
# Debian / Ubuntu
sudo apt install ansible

# Fedora / RHEL
sudo dnf install ansible

# macOS
brew install ansible

Confirm it landed:

bash
ansible --version
text
ansible [core 2.18.3]
  config file = None
  python version = 3.12.3
  jinja version = 3.1.4

The ansible command runs single ad-hoc tasks; ansible-playbook runs the YAML files. Both come from the same install.

Write an inventory

The inventory lists the hosts Ansible can touch, optionally in groups. The simplest possible one is a single line. Create inventory.ini:

ini
[web]
192.0.2.10 ansible_user=techearl

[web] is a group name. The line under it is a host, and ansible_user=techearl tells Ansible which SSH user to connect as. If your SSH key is already on that box for the techearl user (see creating a user on Linux if it is not), this is all the connection detail Ansible needs.

Ansible uses the same SSH that your terminal does, so confirm a plain ssh techearl@192.0.2.10 logs in without a password prompt first; if that asks for one, fix the key before Ansible will get anywhere. Once raw SSH is clean, prove the connection from Ansible's side before writing any playbook. The ping module is not an ICMP ping; it confirms Ansible can SSH in and run Python on the far side:

bash
ansible web -i inventory.ini -m ping
text
192.0.2.10 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

pong means SSH worked and Python is present on the host. If you see a permission or unreachable error instead, fix SSH first; the playbook will not get any further than this did.

Write your first playbook

A playbook is a YAML list of plays. Each play maps a group of hosts to an ordered list of tasks, and each task calls one module. Create playbook.yml:

yaml
---
- name: Set up the web host
  hosts: web
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Make sure nginx is running and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

    - name: Drop in a holding-page index
      ansible.builtin.copy:
        content: "Configured by Ansible.\n"
        dest: /usr/share/nginx/html/index.html
        mode: "0644"

A few things worth naming here. become: true is Ansible's privilege escalation; it runs the tasks through sudo on the host, which you need to install packages and write under /usr/share. That is the same idea as running a command as another user on Linux, just expressed declaratively. The ansible.builtin.package module is distro-agnostic: it dispatches to apt, dnf, or whatever the host actually uses, so the same play installs nginx on Ubuntu and Fedora alike. Every task has a name because those names are what you read in the run output.

Run it

bash
ansible-playbook -i inventory.ini playbook.yml
text
PLAY [Set up the web host] *****************************************************

TASK [Gathering Facts] *********************************************************
ok: [192.0.2.10]

TASK [Install nginx] ***********************************************************
changed: [192.0.2.10]

TASK [Make sure nginx is running and enabled] **********************************
changed: [192.0.2.10]

TASK [Drop in a holding-page index] ********************************************
changed: [192.0.2.10]

PLAY RECAP *********************************************************************
192.0.2.10  : ok=4  changed=3  unreachable=0  failed=0  skipped=0

changed means Ansible did something; ok means the state was already correct. That distinction is the whole point of Ansible, and the reason the next section matters.

Idempotence: why running it twice is safe

Run the exact same command again and the recap flips:

text
PLAY RECAP *********************************************************************
192.0.2.10  : ok=4  changed=0  unreachable=0  failed=0  skipped=0

changed=0. Nothing was touched the second time, because the host already matched the desired state: nginx is installed, the service is running, the file has the right content. This is idempotence. Ansible modules describe the end state you want, then do only the work needed to reach it. You can run the same playbook on every deploy without fear of stacking duplicate changes, which is what separates configuration management from a shell script that blindly re-runs every line.

The honest caveat: idempotence is a property of the modules, not a guarantee Ansible enforces for you. ansible.builtin.package and ansible.builtin.service are idempotent. The ansible.builtin.command and ansible.builtin.shell modules are not; they run whatever you give them every time and report changed on every run unless you add a creates: or changed_when: guard. Reach for the purpose-built module before you reach for shell.

Ad-hoc commands vs playbooks

Not everything needs a playbook. For a one-off ("restart nginx everywhere", "what is the uptime"), an ad-hoc command is faster.

Ad-hoc commandPlaybook
Invocationansible web -m service -a "name=nginx state=restarted"ansible-playbook playbook.yml
Good forOne throwaway task, quick checksRepeatable, version-controlled setup
StoredNothing, it lives in your shell historyA YAML file you commit to git
Multiple stepsOne module per callMany tasks, ordered, in one run
IdempotentSame module rules applySame module rules apply

The rule I use: if I would want to run it again next month, or if anyone else would need to know what I did, it goes in a playbook. Everything else is ad-hoc.

When not to reach for Ansible

Ansible is push-based and runs over SSH on demand. That is the wrong shape for a few jobs. If you need machines to continuously enforce their own state without a control node poking them, a pull-based agent model fits better. If you are provisioning the infrastructure itself (creating the VMs, networks, and load balancers in a cloud), that is a job for an infrastructure-as-code tool, and Ansible configures the boxes after they exist rather than creating them. And for a single one-off command on one machine you already have a shell on, SSH and a script are simply less ceremony. Ansible earns its keep when you have more than one host, want the result to be repeatable, and want the desired state written down.

See also

Sources

Authoritative references this article was fact-checked against.

TagsAnsibleDevOpsAutomationPlaybookSSHConfiguration ManagementLinuxYAML

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

AI for WordPress SEO: The Playbook

The SEO-role playbook for AI on WordPress sites: internal link audits, schema generation, content-gap analysis, redirect audits, title/meta rewrites, technical SEO triage, and AI-search citability. Plus what stays human.

AI for WordPress Agency Operations: The Playbook

The agency-ops playbook for AI: proposals, SOWs, onboarding documents, SOP creation, meeting summaries, status reports, internal documentation. Where the per-hour gain is highest, and the rules that keep client trust intact.

AI for WordPress Sysadmins: The Playbook

The sysadmin-role playbook for AI on WordPress infrastructure: log triage, deploy verification, config audits, backup checks, security sweeps, fail2ban rule generation, and the strict read-only discipline that keeps production safe.