You’ve been in IT long enough to know the drill. A security patch needs to go out. Fifty servers. Each one requires logging in, running the same commands, verifying the same outputs. Four hours later, you’ve done the same thing fifty times and your brain has turned to mush.
There’s a better way. And no, it doesn’t require a computer science degree or becoming a “DevOps engineer” overnight.
Ansible lets you write what you want done once, then run it against one server or a thousand. No agents to install on your target machines. No complex programming required. If you can follow a recipe, you can write an Ansible playbook.
This tutorial assumes you’re a sysadmin or IT professional who’s comfortable with basic Linux commands and wants to stop doing repetitive tasks manually. We’ll build real, usable automation that you can deploy in your environment this week.
Why Ansible Specifically?
The automation landscape is crowded. Puppet, Chef, SaltStack, Terraform—the list goes on. So why focus on Ansible?
Agentless architecture. Most configuration management tools require installing software on every machine you want to manage. Ansible uses SSH (or WinRM for Windows), which means your target servers don’t need anything beyond what they already have. Less software to maintain, fewer attack vectors to worry about.
Human-readable YAML. Ansible playbooks use YAML syntax, which reads almost like English. Compare this to Puppet’s Ruby-based DSL or Chef’s actual Ruby code. You don’t need programming experience to write effective Ansible automation.
Low barrier, high ceiling. You can write your first useful playbook in 30 minutes. But Ansible scales to managing thousands of nodes across complex environments with roles, collections, and advanced orchestration.
Industry adoption. Red Hat owns Ansible, which means enterprise support and continued development. It’s one of the most requested skills on DevOps job postings, and learning it strengthens your position whether you’re staying in traditional sysadmin work or transitioning toward DevOps. Adding Ansible to your skill set also pairs well with IT certifications if you’re building a career path.
Getting Started: Installation and Setup
Installing Ansible
Ansible runs on your control machine—the computer from which you’ll issue commands. This is typically your workstation or a dedicated management server. The target machines (called “managed nodes”) don’t need Ansible installed.
On Ubuntu/Debian:
sudo apt update
sudo apt install ansible
On RHEL/CentOS/Rocky:
sudo dnf install ansible-core
On macOS:
brew install ansible
Using pip (any system with Python):
pip install ansible
Verify your installation:
ansible --version
You should see version information and the path to your configuration file.
SSH Key Setup
Ansible uses SSH to connect to managed nodes. Password authentication works, but SSH keys are more secure and enable true automation without manual intervention.
If you don’t have SSH keys set up:
ssh-keygen -t ed25519 -C "ansible-control"
Copy your public key to each managed node:
ssh-copy-id [email protected]
ssh-copy-id [email protected]
Test that you can SSH without a password:
ssh [email protected]
If you’re managing Windows servers, you’ll configure WinRM instead. That’s a separate topic, but Ansible handles Windows automation equally well once WinRM is enabled. You might also want to check out Group Policy for Windows-native automation.
Your First Inventory File
Ansible needs to know which servers to manage. This information lives in an inventory file. The default location is /etc/ansible/hosts, but you can create project-specific inventories anywhere.
Create a simple inventory file:
# inventory.ini
[webservers]
web1.example.com
web2.example.com
[databases]
db1.example.com
db2.example.com
[all:vars]
ansible_user=admin
This inventory defines two groups: webservers and databases. The [all:vars] section sets variables that apply to all hosts—in this case, the SSH username.
You can also use IP addresses if DNS isn’t configured:
[webservers]
192.168.1.10
192.168.1.11
[databases]
192.168.1.20
Testing Connectivity
Before writing any automation, verify that Ansible can reach your servers. Good networking fundamentals help here if you run into connection issues:
ansible all -i inventory.ini -m ping
The -m ping flag runs Ansible’s ping module, which tests SSH connectivity and Python availability on the target. A successful response looks like:
web1.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
If you see failures, troubleshoot SSH connectivity first. Ansible is just using SSH under the hood.
Writing Your First Playbook
Ad-hoc commands are useful for quick tasks, but playbooks are where Ansible shines. A playbook is a YAML file that describes the desired state of your systems.
Playbook Structure
Create a file called update-packages.yml:
---
- name: Update all packages on web servers
hosts: webservers
become: true
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Upgrade all packages
apt:
upgrade: dist
Let’s break this down:
---indicates the start of a YAML documentname:is a human-readable description of what this play doeshosts: webserverstargets the webservers group from your inventorybecome: trueescalates privileges (like using sudo)tasks:is a list of actions to perform
Each task has:
- A
name:that describes the action - A module (
aptin this case) with its parameters
Running a Playbook
Execute your playbook:
ansible-playbook -i inventory.ini update-packages.yml
Ansible shows detailed output for each task on each host. You’ll see:
- ok — Task completed, no changes needed (idempotent)
- changed — Task made a change to the system
- failed — Task encountered an error
This idempotency is key. You can run the same playbook multiple times safely. If packages are already updated, Ansible skips the work.
A More Realistic Example
Let’s build something you’d actually use: configuring a web server with Nginx.
---
- name: Configure Nginx web servers
hosts: webservers
become: true
vars:
nginx_port: 80
document_root: /var/www/html
tasks:
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Create document root directory
file:
path: "{{ document_root }}"
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: Copy custom index page
template:
src: templates/index.html.j2
dest: "{{ document_root }}/index.html"
owner: www-data
group: www-data
mode: "0644"
notify: Restart Nginx
- name: Ensure Nginx is running and enabled
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Restart Nginx
service:
name: nginx
state: restarted
New concepts here:
Variables (vars:) let you define values once and reuse them throughout the playbook. The {{ variable_name }} syntax pulls in variable values.
Templates (template: module) process Jinja2 template files, substituting variables before copying to the target. Create templates/index.html.j2:
<!DOCTYPE html>
<html>
<head>
<title>{{ ansible_hostname }}</title>
</head>
<body>
<h1>Welcome to {{ ansible_hostname }}</h1>
<p>Deployed by Ansible</p>
</body>
</html>
The {{ ansible_hostname }} is a fact—system information Ansible gathers automatically.
Handlers run only when notified by a task that made changes. In this example, Nginx restarts only if the index page was actually modified. This avoids unnecessary service disruptions.
Organizing Larger Projects with Roles
Playbooks get messy fast. What starts as a clean 50-line file turns into 500 lines of spaghetti the moment you add a second application. Roles fix this by giving you a standardized way to organize related tasks, templates, files, and variables.
Role Directory Structure
Ansible expects roles in a specific directory layout:
roles/
└── nginx/
├── tasks/
│ └── main.yml
├── handlers/
│ └── main.yml
├── templates/
│ └── nginx.conf.j2
├── files/
│ └── custom-error.html
├── vars/
│ └── main.yml
└── defaults/
└── main.yml
Only include the directories you need. Ansible ignores missing folders.
Creating a Role
Let’s convert our Nginx playbook into a role.
roles/nginx/tasks/main.yml:
---
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Copy Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart Nginx
- name: Ensure Nginx is running
service:
name: nginx
state: started
enabled: yes
roles/nginx/handlers/main.yml:
---
- name: Restart Nginx
service:
name: nginx
state: restarted
roles/nginx/defaults/main.yml:
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
Now your playbook becomes simple:
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- nginx
Roles are reusable across projects. You can also install community roles from Ansible Galaxy, which provides thousands of pre-built roles for common tasks.
ansible-galaxy install geerlingguy.docker
This downloads a well-maintained Docker role that handles installation across different Linux distributions.
Practical Automation Examples
Theory is fine, but let’s build automation you’ll actually use.
Example 1: User Management
Managing users across many servers is tedious and error-prone when done manually.
---
- name: Manage system users
hosts: all
become: true
vars:
users:
- username: jsmith
groups: ["sudo", "docker"]
state: present
- username: mjones
groups: ["developers"]
state: present
- username: former_employee
state: absent
tasks:
- name: Create or remove users
user:
name: "{{ item.username }}"
groups: "{{ item.groups | default([]) }}"
state: "{{ item.state }}"
shell: /bin/bash
loop: "{{ users }}"
when: item.state == 'present'
- name: Remove departed users
user:
name: "{{ item.username }}"
state: absent
remove: yes
loop: "{{ users }}"
when: item.state == 'absent'
- name: Deploy SSH keys for active users
authorized_key:
user: "{{ item.username }}"
key: "{{ lookup('file', 'files/ssh_keys/' + item.username + '.pub') }}"
loop: "{{ users }}"
when: item.state == 'present'
Keep SSH public keys in files/ssh_keys/jsmith.pub, etc. When someone leaves, change their state to absent and run the playbook. Their account disappears from all servers.
Example 2: Security Hardening
If you’re interested in cybersecurity careers, automation skills are a huge plus. Here’s how to apply consistent security settings across your infrastructure:
---
- name: Apply security hardening
hosts: all
become: true
tasks:
- name: Ensure SSH root login is disabled
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PermitRootLogin"
line: "PermitRootLogin no"
notify: Restart SSH
- name: Disable password authentication
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PasswordAuthentication"
line: "PasswordAuthentication no"
notify: Restart SSH
- name: Set SSH idle timeout
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?ClientAliveInterval"
line: "ClientAliveInterval 300"
notify: Restart SSH
- name: Install and enable fail2ban
apt:
name: fail2ban
state: present
- name: Configure fail2ban for SSH
template:
src: templates/jail.local.j2
dest: /etc/fail2ban/jail.local
notify: Restart fail2ban
- name: Ensure firewall is enabled
ufw:
state: enabled
policy: deny
- name: Allow SSH through firewall
ufw:
rule: allow
port: 22
proto: tcp
handlers:
- name: Restart SSH
service:
name: sshd
state: restarted
- name: Restart fail2ban
service:
name: fail2ban
state: restarted
Run this against your home lab to practice before deploying to production.
Example 3: Application Deployment
Deploy a Python application with all its dependencies:
---
- name: Deploy Python application
hosts: app_servers
become: true
vars:
app_name: myapp
app_user: appuser
app_dir: /opt/{{ app_name }}
repo_url: https://github.com/company/myapp.git
venv_path: "{{ app_dir }}/venv"
tasks:
- name: Install system dependencies
apt:
name:
- python3
- python3-pip
- python3-venv
- git
state: present
- name: Create application user
user:
name: "{{ app_user }}"
system: yes
shell: /bin/bash
- name: Clone application repository
git:
repo: "{{ repo_url }}"
dest: "{{ app_dir }}"
version: main
become_user: "{{ app_user }}"
- name: Create virtual environment
command: python3 -m venv {{ venv_path }}
args:
creates: "{{ venv_path }}/bin/python"
become_user: "{{ app_user }}"
- name: Install Python dependencies
pip:
requirements: "{{ app_dir }}/requirements.txt"
virtualenv: "{{ venv_path }}"
become_user: "{{ app_user }}"
- name: Copy systemd service file
template:
src: templates/app.service.j2
dest: /etc/systemd/system/{{ app_name }}.service
notify:
- Reload systemd
- Restart application
- name: Enable and start application
service:
name: "{{ app_name }}"
state: started
enabled: yes
handlers:
- name: Reload systemd
systemd:
daemon_reload: yes
- name: Restart application
service:
name: "{{ app_name }}"
state: restarted
This pattern works for any application deployment. The git module handles pulling code, pip manages Python dependencies, and systemd ensures the app runs on boot.
Variables and Facts: Making Playbooks Flexible
Hardcoded values make playbooks brittle. Ansible provides multiple ways to parameterize your automation.
Variable Precedence
Ansible has 22 levels of variable precedence. The most common ones, in order of increasing priority:
- Role defaults (
roles/x/defaults/main.yml) - Inventory group variables
- Inventory host variables
- Playbook
vars: - Extra variables (
-eflag)
This means you can set sensible defaults in roles but override them for specific hosts or environments.
Inventory Variables
Define variables for groups or individual hosts:
[webservers]
web1.example.com nginx_worker_processes=4
web2.example.com nginx_worker_processes=2
[webservers:vars]
nginx_port=80
For complex variable structures, use group_vars and host_vars directories:
inventory/
├── production.ini
├── group_vars/
│ ├── webservers.yml
│ └── databases.yml
└── host_vars/
└── web1.example.com.yml
Ansible Facts
Ansible automatically gathers information about managed hosts. Access these facts in your playbooks:
- name: Display system information
debug:
msg: |
Hostname: {{ ansible_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Memory: {{ ansible_memtotal_mb }} MB
CPUs: {{ ansible_processor_vcpus }}
Use facts for conditional logic:
- name: Install package (Debian/Ubuntu)
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install package (RHEL/CentOS)
dnf:
name: nginx
state: present
when: ansible_os_family == "RedHat"
This single playbook works across different Linux distributions.
Debugging and Troubleshooting
When playbooks don’t work as expected, Ansible provides several debugging tools.
Verbose Output
Add -v flags for more detail:
ansible-playbook playbook.yml -v # Basic verbose
ansible-playbook playbook.yml -vv # More detail
ansible-playbook playbook.yml -vvv # Connection debugging
ansible-playbook playbook.yml -vvvv # Maximum verbosity
The Debug Module
Print variable values during execution:
- name: Show variable value
debug:
var: my_variable
- name: Show formatted message
debug:
msg: "The value is {{ my_variable }}"
Check Mode (Dry Run)
See what would happen without making changes:
ansible-playbook playbook.yml --check
Some modules don’t support check mode, but most core modules do.
Step-by-Step Execution
Run tasks one at a time with confirmation:
ansible-playbook playbook.yml --step
Limiting Scope
Test against a single host before rolling out widely:
ansible-playbook playbook.yml --limit web1.example.com
Integrating Ansible Into Your Workflow
Ansible becomes most powerful when integrated into your daily operations.
Version Control
Store playbooks, inventory, and roles in Git. This provides:
- History of changes
- Collaboration with teammates
- Rollback capability
- Code review for infrastructure changes
Structure your repository:
ansible-repo/
├── ansible.cfg
├── inventory/
│ ├── production/
│ └── staging/
├── playbooks/
├── roles/
├── group_vars/
├── host_vars/
└── README.md
CI/CD Integration
Run Ansible from your CI/CD pipeline for automated deployments. Jenkins, GitLab CI, GitHub Actions, and other platforms all support Ansible execution. If you’re looking to move into DevOps, CI/CD integration is where Ansible really shines.
A basic GitHub Actions workflow:
name: Deploy Application
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install Ansible
run: pip install ansible
- name: Run deployment
run: ansible-playbook -i inventory/production playbooks/deploy.yml
env:
ANSIBLE_HOST_KEY_CHECKING: false
Documentation
Well-written playbooks document themselves. Use meaningful names for plays, tasks, and variables. Add comments for complex logic:
# This task handles the edge case where the config file
# doesn't exist on first run. We create it from template
# if missing, but preserve any existing customizations.
- name: Ensure configuration file exists
template:
src: config.j2
dest: /etc/app/config.yml
force: no # Don't overwrite existing files
Common Mistakes and How to Avoid Them
After helping a few dozen people learn Ansible, the same mistakes come up again and again. Save yourself some frustration.
Forgetting become: true: Many tasks require root privileges. If you see permission denied errors, add become: true to your play or task.
YAML indentation errors: YAML is whitespace-sensitive. Use two spaces for indentation, never tabs. A good editor with YAML support prevents most issues. If you’re coming from PowerShell, the syntax takes some getting used to.
Not testing incrementally: Write a few tasks, test them, then add more. Debugging a 200-task playbook at 2 AM is painful. Ask me how I know.
Ignoring idempotency: Ansible modules are designed to be idempotent—running them multiple times produces the same result. If you find yourself using shell: or command: modules frequently, look for a proper Ansible module instead.
Hardcoding everything: Use variables from the start. It’s easier to parameterize as you write than to refactor later.
Skipping error handling: Add ignore_errors: yes only when appropriate. Consider failed_when: and changed_when: for fine-grained control over task status.
Next Steps: Where to Go From Here
You’ve got the basics. Here’s how to keep building.
Practice in a lab environment. Set up a few VMs in VirtualBox or on a cloud free tier. Break things. Fix them with automation.
Explore Ansible Galaxy. Browse galaxy.ansible.com for community roles. Reading well-written roles teaches advanced patterns.
Automate something real. Pick one manual task you do weekly. Automate it. Then pick another. Compound benefits build quickly.
Learn complementary tools. Ansible pairs well with Terraform for infrastructure provisioning and Docker for containerized deployments. Understanding all three makes you significantly more effective.
Join the community. The Ansible subreddit, Discord servers, and the official forum are active and welcoming to questions. Building skills like this also strengthens your resume for IT roles.
If you want to build stronger command line skills that complement your Ansible work, Shell Samurai offers interactive terminal challenges that build the muscle memory you need for effective system administration.
FAQ
Do I need to know Python to use Ansible?
No. You can be productive with Ansible knowing zero Python. Playbooks use YAML, not Python. However, understanding basic Python helps when debugging issues or writing custom modules.
Can Ansible manage Windows servers?
Yes. Ansible uses WinRM instead of SSH for Windows targets. Most core modules have Windows equivalents. You’ll need to configure WinRM on your Windows servers and potentially install additional Python packages on your control node.
How does Ansible compare to Terraform?
They solve different problems. Terraform provisions infrastructure (creating VMs, networks, storage), while Ansible configures what’s running on that infrastructure. Many teams use both: Terraform creates servers, then Ansible configures them. Some overlap exists in cloud provider modules.
Is Ansible suitable for small environments?
Absolutely. Ansible’s lack of required infrastructure (no server, no agents) makes it perfect for small environments. Even managing five servers benefits from codified, repeatable configurations. This is why it’s great for home labs too.
How do I handle sensitive data like passwords?
Ansible Vault encrypts sensitive files or variables. Create an encrypted variable file with ansible-vault create secrets.yml, then reference it in your playbooks. Vault supports password files for CI/CD integration.
What’s the difference between roles and collections?
Roles organize content for a single purpose (like configuring Nginx). Collections are broader—they bundle multiple roles, modules, and plugins into a distributable package. Collections became the standard distribution format in Ansible 2.10+.
Your first automation project doesn’t need to be complicated. Pick one repetitive task—installing packages, creating users, copying configurations—and write a playbook for it. Run it manually until you trust it. Then schedule it or integrate it into your workflow.
The goal isn’t to automate everything overnight. It’s to stop doing the same thing by hand more than twice.