Ansible Tutorial for Beginners — Automate Servers Without Writing a Custom Agent
Learn Ansible from scratch: inventory, playbooks, modules, and roles. Automate Linux configuration with YAML and SSH—no daemon required.
Ansible is one of the most approachable configuration management tools because it does not require installing a long-running agent on every server. You describe desired state in YAML playbooks, Ansible connects over SSH (or WinRM on Windows), and idempotent modules make changes only when needed.
If you are tired of SSH-ing into ten machines to update a package or drop a config file, this tutorial walks you from zero to a repeatable deployment workflow.
What Ansible does
Ansible automates:
- Provisioning — users, packages, services, firewall rules
- Application deployment — copy artifacts, restart services, run migrations
- Orchestration — ordered multi-step workflows across groups of hosts
- Ad hoc commands — one-off tasks without writing a full playbook
The control node (your laptop or CI runner) holds playbooks and inventory. Managed nodes need Python and SSH access—nothing else.
Install Ansible on the control node
On Ubuntu or Debian:
sudo apt update && sudo apt install -y ansible
ansible --version
On macOS with Homebrew:
brew install ansible
For learning, a control node and one or two VMs (local VirtualBox, cloud EC2, or Multipass) are enough.
Inventory: naming your servers
Inventory lists hosts Ansible manages. The simplest form is an INI file:
[web]
web1.example.com
web2.example.com
[db]
db1.example.com
[web:vars]
ansible_user=ubuntu
YAML inventory works too:
all:
children:
web:
hosts:
web1.example.com:
web2.example.com:
db:
hosts:
db1.example.com:
Test connectivity:
ansible all -m ping -i inventory.ini
A successful pong means SSH and Python are reachable.
Your first playbook
Playbooks are YAML files describing plays—ordered tasks targeting host groups.
Create site.yml:
---
- name: Basic web server setup
hosts: web
become: yes
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: yes
- name: Deploy index page
ansible.builtin.copy:
dest: /var/www/html/index.html
content: "<h1>Hello from Ansible</h1>\n"
mode: '0644'
notify: Reload nginx
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Run it:
ansible-playbook -i inventory.ini site.yml
Key concepts in this playbook:
become: yes— escalate privileges (sudo)state: present— idempotent install; skips if already installednotify/handlers— run reload only when the copy task changes
Run the playbook again. Ansible should report ok and changed=0 on most tasks—that is idempotency working.
Modules you will use constantly
| Module | Purpose |
|---|---|
package / apt / yum | Install software |
copy / template | Deploy files; template renders Jinja2 |
service | Start/stop/restart systemd units |
user / group | Account management |
file | Permissions, symlinks, directories |
command / shell | Run commands (prefer dedicated modules when they exist) |
Favor dedicated modules over raw shell—they return structured changed/ok status and handle edge cases.
Variables and facts
Define variables in playbooks, inventory, group_vars, or host_vars:
# group_vars/web.yml
app_port: 8080
Reference with Jinja2:
- name: Open app port
ansible.builtin.iptables:
chain: INPUT
protocol: tcp
destination_port: "{{ app_port }}"
jump: ACCEPT
Ansible gathers facts about each host (OS, IP addresses, mount points):
ansible web -m setup -i inventory.ini | less
Use ansible_facts['distribution'] in templates for OS-specific logic.
Templates with Jinja2
For config files with per-environment values, use template:
# templates/nginx.conf.j2
server {
listen {{ app_port }};
server_name {{ ansible_hostname }};
root /var/www/html;
}
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: Reload nginx
Roles: organizing larger projects
When playbooks grow, split logic into roles—reusable bundles of tasks, handlers, templates, and defaults:
roles/
nginx/
tasks/main.yml
handlers/main.yml
templates/nginx.conf.j2
defaults/main.yml
A play imports roles:
- hosts: web
become: yes
roles:
- nginx
- app
Generate role scaffolding:
ansible-galaxy init nginx
Community roles on Ansible Galaxy (ansible-galaxy install geerlingguy.docker) accelerate common stacks—pin versions in requirements.yml for reproducibility.
Ad hoc tasks
Quick one-liners without a playbook:
# Restart nginx on all web servers
ansible web -i inventory.ini -b -m service -a "name=nginx state=restarted"
# Check disk usage
ansible all -i inventory.ini -m command -a "df -h"
-b is shorthand for --become.
Ansible Vault for secrets
Never commit plaintext passwords. Encrypt sensitive files:
ansible-vault create group_vars/web/vault.yml
ansible-playbook site.yml --ask-vault-pass
In CI, pass the vault password via environment variable or secret manager integration.
Running from CI/CD
A typical pipeline stage:
- Lint playbooks with
ansible-lint - Run against a staging inventory
- Manual approval gate
- Run against production with limited fork count and serial strategy for rolling updates
- hosts: web
serial: 2 # update two at a time
max_fail_percentage: 0
Store inventory per environment (inventories/staging, inventories/prod) and pass -i inventories/prod/hosts.
Ansible vs Terraform vs shell scripts
- Shell scripts run once; re-running may duplicate work or break state.
- Terraform provisions cloud infrastructure (VPCs, instances, DNS).
- Ansible configures whatever already exists—or pairs with Terraform output for dynamic inventory.
Many teams Terraform the VMs, then Ansible configures them.
Common pitfalls
SSH host key checking in lab environments
For throwaway labs only, you might disable strict host key checking in ansible.cfg. In production, manage known_hosts properly.
Running as root everywhere
Use become selectively; define a sudo-capable deploy user.
Non-idempotent shell tasks
command: ./install.sh runs every time unless you add creates: or a checks file.
Huge playbooks in one file
Refactor into roles early.
Practice project
Solidify skills with this sequence:
- Two Ubuntu VMs in a
webgroup - Playbook installs nginx and a static site
- Extract nginx tasks into a role
- Add a
templatefor virtual host config with a variable port - Encrypt a fake API key with Vault and template it into an app env file
- Run the playbook twice and confirm zero changes on the second run
FAQ
Does Ansible work on Windows?
Yes, via WinRM. Linux-style SSH workflows are still the most common starting point.
How does Ansible compare to Chef or Puppet?
Chef and Puppet traditionally relied on agents and richer DSLs. Ansible's agentless SSH model lowers the adoption bar.
Can Ansible replace Docker?
No—they complement each other. Ansible can install Docker and deploy containers; Kubernetes often uses Helm or operators for in-cluster state.
Is YAML indentation really that strict?
Yes. Use spaces, not tabs, and validate with ansible-playbook --syntax-check.
Debugging failed runs
When a task fails, Ansible stops the play unless ignore_errors: yes is set. Read the stderr output carefully—permission errors often mean missing become, and unreachable usually means SSH or firewall issues.
Re-run with verbose output to see module arguments and connection details:
ansible-playbook -i inventory.ini site.yml -vvv
Use --check (dry run) and --diff to preview changes without applying them. Combine both during change review windows.
Next steps
After this tutorial, explore dynamic inventory (AWS EC2 plugin), Molecule for role testing, and AWX or Ansible Automation Platform for centralized job scheduling. The core loop stays the same: describe desired state, run the playbook, let idempotency keep production drift in check.
Comments
Loading comments…