Disclosure: some links on this page are affiliate links. If you sign up through them we may earn a commission at no extra cost to you. It helps keep LinuxDistroFinder free.
You've just spun up three fresh Ubuntu servers. You need to install Nginx, create a deploy user, harden SSH, open the right firewall ports, and deploy your app — on all three, identically. Doing it by hand takes 45 minutes and you'll make at least one mistake. Ansible does it in under two minutes, repeatably, every time.
Ansible is an open-source IT automation tool written in Python. It's agentless — you install nothing on the servers you manage. It communicates over plain SSH, reads human-readable YAML files called playbooks, and applies your desired state to any number of machines. That last part is the key: Ansible is idempotent, meaning running the same playbook twice produces the same result without breaking anything on the second run.
This guide walks you from zero to a working server-setup playbook. We'll cover installation, inventory files, ad-hoc commands, modules, roles, and best practices — with real terminal commands and real YAML throughout.
Core Concepts in Plain English
Before writing a single line of YAML, it helps to understand Ansible's four moving parts:
| Concept | What it is | File / location |
|---|---|---|
| Control node | The machine you run Ansible from (your laptop or a jump host) | Needs Python 3.9+ and Ansible installed |
| Managed nodes | The servers being configured — no Ansible installed here | Needs SSH access and Python 3 on the remote |
| Inventory | A list of managed nodes, optionally grouped | inventory.ini or inventory/ directory |
| Playbook | A YAML file describing what to do on which hosts | site.yml, setup.yml, etc. |
| Module | A built-in unit of work (install package, create user, copy file…) | Called inside tasks in a playbook |
| Role | A reusable, self-contained bundle of tasks, templates, and variables | roles/rolename/ directory |
Installing Ansible on Your Control Node
Ansible only needs to be on the machine you're running commands from. The most reliable way to install it in 2026 is via pipx, which keeps it isolated from your system Python. Alternatively, most distro package managers carry a recent-enough version.
Ubuntu / Debian
# Option A — official PPA (usually newer than universe)
sudo apt update
sudo apt install software-properties-common -y
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible -y
# Confirm version
ansible --versionFedora / RHEL 9+ / AlmaLinux / Rocky
sudo dnf install ansible -y
ansible --versionAny distro — via pipx (recommended for latest stable)
sudo apt install pipx -y # or dnf install pipx
pipx install ansible
pipx ensurepath
# Restart your shell, then:
ansible --versionAs of mid-2026, the current stable release is Ansible 10.x (which bundles ansible-core 2.17). You want at least ansible-core 2.16 to get full support for the ansible.builtin.dnf5 module on RHEL 10 and AlmaLinux 10.
pip install ansible directly into your system Python — it can conflict with distro packages. Use pipx or a virtualenv instead.Setting Up SSH Key Authentication
Ansible uses SSH. Password-based SSH works but is painfully slow at scale — use key-based auth. If you haven't already:
# Generate a key pair on your control node (if you don't have one)
ssh-keygen -t ed25519 -C "ansible-control" -f ~/.ssh/ansible_key
# Copy the public key to each managed node
ssh-copy-id -i ~/.ssh/ansible_key.pub user@192.168.1.101
ssh-copy-id -i ~/.ssh/ansible_key.pub user@192.168.1.102
ssh-copy-id -i ~/.ssh/ansible_key.pub user@192.168.1.103If you're on a VPS provider like Vultr or Hostinger, you can paste the public key during server creation so it's there from first boot — which means zero manual SSH setup before running Ansible.
Your First Inventory File
An inventory file tells Ansible which hosts exist and how to reach them. The simplest format is INI:
# inventory.ini
[webservers]
web1 ansible_host=192.168.1.101
web2 ansible_host=192.168.1.102
[dbservers]
db1 ansible_host=192.168.1.103
[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/ansible_key
ansible_python_interpreter=/usr/bin/python3Groups like [webservers] let you target subsets of your fleet. [all:vars] sets variables that apply to every host. You can also write inventories in YAML — useful when you have dozens of groups — but INI is easier to start with.
Test connectivity with a ping
# -i specifies the inventory file, 'all' targets every host
ansible -i inventory.ini all -m pingYou should see pong back from each host. If you see UNREACHABLE, double-check your SSH key path and that the user exists on the remote.
Ad-hoc Commands — Ansible Without a Playbook
Ad-hoc commands are one-liners for quick tasks. They use the -m flag to call a module and -a for arguments. They're great for exploring or doing emergency one-offs:
# Check uptime on all webservers
ansible -i inventory.ini webservers -m command -a "uptime"
# Install htop on all hosts (become = sudo)
ansible -i inventory.ini all -m ansible.builtin.apt -a "name=htop state=present" --become
# Restart nginx on webservers
ansible -i inventory.ini webservers -m ansible.builtin.service -a "name=nginx state=restarted" --become
# Copy a file to all hosts
ansible -i inventory.ini all -m ansible.builtin.copy -a "src=./motd.txt dest=/etc/motd" --becomeansible.builtin.apt) in production playbooks. It makes the collection source unambiguous and avoids surprises when community collections are also installed.Writing Your First Playbook
A playbook is a YAML file with one or more plays. Each play targets a group of hosts and lists tasks to run on them. Here's a complete playbook that sets up a production-ready Nginx web server from a blank Ubuntu 24.04 VPS:
# setup-webserver.yml
---
- name: Configure web servers
hosts: webservers
become: true # run tasks as root (via sudo)
vars:
app_user: deploy
nginx_port: 80
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600 # only update if cache is older than 1 hour
- name: Install required packages
ansible.builtin.apt:
name:
- nginx
- ufw
- fail2ban
- curl
- git
state: present
- name: Create deploy user
ansible.builtin.user:
name: "{{ app_user }}"
shell: /bin/bash
create_home: true
groups: www-data
append: true
- name: Allow SSH through UFW
community.general.ufw:
rule: allow
name: OpenSSH
- name: Allow HTTP through UFW
community.general.ufw:
rule: allow
port: "{{ nginx_port }}"
proto: tcp
- name: Enable UFW
community.general.ufw:
state: enabled
policy: deny
- name: Ensure nginx is started and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Ensure fail2ban is started and enabled
ansible.builtin.service:
name: fail2ban
state: started
enabled: true
- name: Harden SSH — disable password auth
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
backup: true
notify: Restart sshd
handlers:
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restartedRun it with:
ansible-playbook -i inventory.ini setup-webserver.ymlAnsible prints a coloured summary — green for ok, yellow for changed, red for failed. A task that is already in the desired state shows ok and makes no change. That's idempotency in action.
What each section means
- vars: — define reusable variables; reference them as
{{ variable_name }} - tasks: — the ordered list of work to do
- notify / handlers: — a handler runs once at the end of the play only if its task triggered a change; perfect for service restarts
- become: true — escalates to root via sudo; can be set per-task or per-play
The Most Useful Built-in Modules
Ansible ships with hundreds of modules. These ten cover 90% of server setup tasks:
| Module | What it does | Key args |
|---|---|---|
ansible.builtin.apt |
Manage packages on Debian/Ubuntu | name, state, update_cache |
ansible.builtin.dnf |
Manage packages on Fedora/RHEL/Rocky | name, state |
ansible.builtin.copy |
Copy a file from control node to remote | src, dest, mode, owner |
ansible.builtin.template |
Copy a Jinja2 template and render variables | src, dest |
ansible.builtin.lineinfile |
Ensure a specific line exists in a file | path, regexp, line |
ansible.builtin.user |
Manage user accounts | name, shell, groups, state |
ansible.builtin.service |
Start/stop/enable systemd services | name, state, enabled |
ansible.builtin.git |
Clone or update a git repository | repo, dest, version |
ansible.builtin.file |
Manage files, directories, symlinks, permissions | path, state, mode, owner |
ansible.builtin.command |
Run a raw command (use sparingly — not idempotent) | cmd, creates, removes |
ansible.builtin.shell over command only when you need pipes or shell expansion. Both are "last resort" modules — if a dedicated module exists for the task, use it. Purpose-built modules are idempotent; raw commands rarely are.Organising with Roles
Once your playbook grows past ~50 tasks, split it into roles. A role is a directory with a standard layout that Ansible recognises automatically:
# Create a role scaffold with ansible-galaxy
ansible-galaxy init roles/nginx
ansible-galaxy init roles/common
ansible-galaxy init roles/deploy_userThis creates a directory tree like:
roles/
nginx/
tasks/
main.yml # task list
handlers/
main.yml # handlers
templates/
nginx.conf.j2 # Jinja2 templates
files/ # static files to copy
vars/
main.yml # role-scoped variables
defaults/
main.yml # default variable values (lowest precedence)
meta/
main.yml # role metadata and dependenciesThen your top-level playbook becomes beautifully concise:
# site.yml
---
- name: Configure all web servers
hosts: webservers
become: true
roles:
- common
- deploy_user
- nginx
- name: Configure database servers
hosts: dbservers
become: true
roles:
- common
- postgresVariables and Vault — Keeping Secrets Safe
Never hardcode passwords or API keys in playbooks. Use ansible-vault to encrypt sensitive variable files:
# Create an encrypted vars file
ansible-vault create group_vars/all/vault.yml
# Edit it later
ansible-vault edit group_vars/all/vault.yml
# Run a playbook that uses vault-encrypted vars
ansible-playbook -i inventory.ini site.yml --ask-vault-pass
# Or store the password in a file (don't commit this file!)
echo "mysecretpassword" > ~/.vault_pass
chmod 600 ~/.vault_pass
ansible-playbook -i inventory.ini site.yml --vault-password-file ~/.vault_passInside your vault file, store secrets like database passwords or API tokens. Reference them in playbooks as normal variables — Ansible decrypts them at runtime.
.vault_pass and any unencrypted secret files to your .gitignore immediately. Vault-encrypted files are safe to commit — the raw password file is not.Common Patterns and Best Practices
--check first
Safety
Dry-run mode (--check) shows what would change without making any changes. Add --diff to also see line-by-line diffs for file tasks. Run this before every production deployment.
group_vars and host_vars directories
Organisation
Ansible automatically loads group_vars/webservers.yml for all hosts in the webservers group, and host_vars/web1.yml for the host named web1. This keeps your inventory clean and your variables contextual.
Add tags: [nginx, packages] to tasks, then run only tagged tasks with --tags nginx. This lets you re-run just the nginx configuration without touching unrelated tasks — critical for fast iteration.
Create a requirements.yml and install collections with ansible-galaxy collection install -r requirements.yml. Specify exact versions to avoid breakage when community collections release updates.
Install ansible-lint via pipx and run it on your playbooks. It catches common mistakes like missing name: fields, deprecated module names, and tasks that aren't idempotent. Add it to your CI pipeline.
# Install ansible-lint
pipx install ansible-lint
# Lint a playbook
ansible-lint setup-webserver.yml
# Dry-run with diff before deploying
ansible-playbook -i inventory.ini site.yml --check --diffNeed Servers to Practice On?
🚀 Get Cloud VPS Instances to Practice Ansible On
The best way to learn Ansible is against real servers — not VMs on your laptop. Spin up 2–3 cheap VPS instances, practice provisioning them end-to-end, then destroy them. Vultr's $2.50/month instances are perfect for this.
Get $100 Free Credit on Vultr →Also great: Hostinger VPS — KVM-based, NVMe storage, starts at ₹299/mo. Both work perfectly as Ansible managed nodes.
Frequently Asked Questions
Do I need to install anything on the servers I'm managing?
Almost nothing. Your managed nodes need SSH access enabled and Python 3 installed — both are present by default on Ubuntu 20.04+, Debian 11+, AlmaLinux 8+, and most modern server distros. Ansible itself only lives on the control node. This is what "agentless" means and it's a major advantage over tools like Puppet or Chef.
What's the difference between Ansible and Terraform?
Terraform is an infrastructure provisioning tool — it creates and destroys cloud resources (VMs, networks, DNS records, load balancers). Ansible is a configuration management tool — it configures software on machines that already exist. In practice, they complement each other: Terraform creates your servers, Ansible configures them. You can also use Ansible's cloud modules to provision infrastructure, but Terraform's state management is generally better for that job.
How is Ansible different from a shell script?
Shell scripts are imperative — you write every step explicitly and they're not idempotent (running adduser deploy twice errors on the second run). Ansible is declarative — you describe the desired state and Ansible figures out whether any change is needed. Ansible also handles error reporting, parallel execution, secrets management, and templating in ways that shell scripts require significant extra code to replicate.
What is ansible-core vs ansible (the full package)?
ansible-core is the minimal engine — the CLI tools, a small set of built-in modules, and the runtime. The ansible package bundles ansible-core plus a curated set of community collections (hundreds of extra modules for AWS, GCP, Docker, Kubernetes, networking gear, etc.). For most beginners, install ansible to get the collections pre-loaded. For a lean CI environment, install just ansible-core and add specific collections via requirements.yml.
Can I use Ansible with Windows servers?
Yes, but instead of SSH, Ansible uses WinRM (Windows Remote Management) or, from Ansible 2.18+, experimental SSH support on Windows. You need to enable WinRM on the target Windows machines and use the ansible.windows collection. It works but is noticeably more complex to set up than Linux automation. Linux is Ansible's native environment.
How do I handle different Linux distros in one playbook?
Use the ansible_os_family or ansible_distribution facts with a when: condition, or create separate task files and include them conditionally. A common pattern is to have tasks/install-debian.yml and tasks/install-redhat.yml and include the right one based on ansible_os_family == "Debian". Ansible's package module also abstracts over apt/dnf/yum for basic installs.
Is there a GUI for Ansible?
The official GUI is AWX (the open-source upstream of Red Hat Ansible Automation Platform). AWX provides a web interface for running playbooks, managing inventory, scheduling jobs, and controlling access. It runs as a containerised application. For small teams or personal use, most people stick with the CLI — it's faster and easier to script into CI/CD pipelines.