$linuxjunkies
>

Install Ansible and Run Your First Playbook

Install Ansible via pip or your distro package manager, build an inventory file, then write and run your first idempotent playbook with privilege escalation.

BeginnerUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • SSH access to at least one remote Linux host (the managed node)
  • Python 3.10 or later on the control machine
  • sudo privileges on both the control machine and managed nodes
  • Network connectivity between control machine and managed nodes on port 22

Ansible lets you automate configuration and deployments with plain YAML files and SSH — no agent required on the managed nodes. This guide walks through installing Ansible cleanly, wiring up an inventory, and running a real playbook, covering the concepts that trip up beginners (idempotency, privilege escalation, and why pip often beats the distro package).

pip vs Distro Package: Which to Use

Distro packages lag behind upstream. Debian 12 ships Ansible 7.x; PyPI has the current 9.x series. For anything beyond a quick experiment, install from PyPI inside a virtual environment. This isolates Ansible's dependencies and lets you upgrade without touching system Python.

The only reason to prefer the distro package is when your org's policy forbids PyPI access or you need the distro's security-patched builds. If that's you, the distro install is at the bottom of this section.

First ensure Python 3.10+ and pip are present, then create a virtual environment.

# Debian / Ubuntu
sudo apt update && sudo apt install -y python3 python3-pip python3-venv
# Fedora / RHEL 9 / Rocky 9
sudo dnf install -y python3 python3-pip
# Arch
sudo pacman -Sy python python-pip

Now create the virtual environment and install Ansible inside it:

python3 -m venv ~/.ansible-venv
source ~/.ansible-venv/bin/activate
pip install --upgrade pip
pip install ansible

Add the activation line to your shell profile so it persists across sessions:

echo 'source ~/.ansible-venv/bin/activate' >> ~/.bashrc
source ~/.bashrc

Verify:

ansible --version

You'll see a block starting with ansible [core X.Y.Z] followed by Python path and module locations. The exact version will vary.

Install via distro package (alternative)

# Debian / Ubuntu
sudo apt install -y ansible
# Fedora / RHEL (enable EPEL first on RHEL/Rocky)
sudo dnf install -y epel-release && sudo dnf install -y ansible
# Arch
sudo pacman -Sy ansible

Set Up SSH Key Authentication

Ansible communicates over SSH. Password auth works but key-based auth is faster and scriptable. If you already have a key pair, skip generation.

ssh-keygen -t ed25519 -C "ansible" -f ~/.ssh/ansible_ed25519
# Copy the public key to each managed node
ssh-copy-id -i ~/.ssh/ansible_ed25519.pub [email protected]

Test the connection manually before involving Ansible:

ssh -i ~/.ssh/ansible_ed25519 [email protected] 'echo connected'

Build Your Inventory File

The inventory tells Ansible which hosts to manage and how to reach them. INI format is simplest for small setups; YAML scales better. Create a project directory to keep everything together:

mkdir ~/ansible-lab && cd ~/ansible-lab

Create inventory.ini:

cat > inventory.ini << 'EOF'
[webservers]
web1 ansible_host=192.168.1.50 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/ansible_ed25519
web2 ansible_host=192.168.1.51 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/ansible_ed25519

[dbservers]
db1 ansible_host=192.168.1.60 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/ansible_ed25519

[all:vars]
ansible_python_interpreter=/usr/bin/python3
EOF

Ping all hosts to confirm connectivity:

ansible -i inventory.ini all -m ping

A successful response looks like web1 | SUCCESS => {"ping": "pong"} for each host.

Understand Idempotency Before Writing Playbooks

Idempotency is Ansible's core promise: running a playbook ten times produces the same result as running it once. Ansible modules track state, not commands. Instead of apt-get install nginx, you declare state: present. If nginx is already installed, Ansible reports ok and does nothing. A changed system reports changed. This distinction matters for auditing and for writing safe automation.

Avoid the shell and command modules for tasks that have a proper module (package, service, file, copy, template). Raw shell commands are not idempotent by default.

Write Your First Playbook

This playbook installs nginx on the webservers group, ensures it is started and enabled, and drops a custom index page. It uses become: true for privilege escalation (sudo) on the remote host.

cat > site.yml << 'EOF'
---
- name: Configure web servers
  hosts: webservers
  become: true

  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Ensure nginx is started and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

    - name: Deploy index page
      ansible.builtin.copy:
        content: "

Managed by Ansible

\n" dest: /var/www/html/index.html owner: root group: root mode: '0644' EOF

A note on become

become: true at the play level tells Ansible to escalate privileges for every task in that play — equivalent to prefixing each command with sudo. The remote user must have passwordless sudo, or you must pass --ask-become-pass at runtime. To grant passwordless sudo on a managed node:

# Run this ON the managed node
echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/ansible-user

Run the Playbook

ansible-playbook -i inventory.ini site.yml

The output shows a play recap per host: ok, changed, unreachable, failed. On the first run you'll see three changed tasks. Run it again immediately:

ansible-playbook -i inventory.ini site.yml

This time every task reports ok=3 changed=0 — idempotency in action. Nothing was modified because the desired state already existed.

Verify the Result

# From the control node
curl http://192.168.1.50/

Expected output: <h1>Managed by Ansible</h1>

You can also run an ad-hoc command to check the nginx service state across the group:

ansible -i inventory.ini webservers -m service_facts | grep -A3 'nginx'

Troubleshooting

  • UNREACHABLE errors: SSH is failing. Test with a raw ssh command using the same key and user. Check firewall rules on the managed node — port 22 must be open.
  • MODULE FAILURE / python not found: The remote node may lack Python 3. Install it, or set ansible_python_interpreter=/usr/bin/python3 in inventory (shown above). On minimal containers, you may need ansible_python_interpreter=/usr/bin/python.
  • sudo: a password is required: Either configure passwordless sudo on the managed node or run with --ask-become-pass.
  • Permission denied (publickey): The SSH key isn't authorized on the remote host. Re-run ssh-copy-id or manually append the public key to ~/.ssh/authorized_keys on the node.
  • ansible-playbook: command not found after reboot: The virtualenv activation wasn't sourced. Confirm the source line is in ~/.bashrc (or ~/.zshrc for Zsh) and run source ~/.bashrc.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

Why use pip instead of the distro package for Ansible?
Distro packages often lag months or years behind upstream. The pip install gives you current bug fixes, new modules, and security patches, and a virtual environment keeps it isolated from system Python.
Do managed nodes need Ansible installed on them?
No. Managed nodes only need Python 3 and an SSH server. Ansible pushes temporary modules over SSH, executes them, then removes them. Nothing persists on the node.
What is the difference between become: true at play level vs task level?
Setting become: true on a play escalates every task in that play. Setting it on an individual task only escalates that one task, which is useful when most tasks run as the regular user but a specific one needs root.
How do I pass variables to a playbook at runtime?
Use the --extra-vars flag: ansible-playbook site.yml --extra-vars "nginx_port=8080". Inside the playbook reference it as {{ nginx_port }}. You can also pass a JSON file with --extra-vars @vars.json.
Is it safe to run a playbook against production on the first try?
Use --check (dry-run) mode first: ansible-playbook -i inventory.ini site.yml --check. Ansible will report what would change without touching the system. Combine with --diff to see exact file changes.

Related guides