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.
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.
Install via pip (recommended)
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
sshcommand 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/python3in inventory (shown above). On minimal containers, you may needansible_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-idor manually append the public key to~/.ssh/authorized_keyson the node. - ansible-playbook: command not found after reboot: The virtualenv activation wasn't sourced. Confirm the
sourceline is in~/.bashrc(or~/.zshrcfor Zsh) and runsource ~/.bashrc.
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
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.