Introduction
Ansible automates server configuration, application deployment, and infrastructure management. You write YAML playbooks describing the desired state, and Ansible makes it happen — no agents to install, no complex setup. This guide takes you from zero to automating real infrastructure.
What Is Ansible?
Ansible is an agentless automation tool. It connects to your servers via SSH (or WinRM for Windows), runs tasks, and disconnects. No software to install on managed hosts.
Key benefits:
- Agentless — uses SSH, nothing to install on remote hosts
- YAML-based — human-readable configuration
- Idempotent — safe to run multiple times (same result)
- Batteries included — 3,000+ modules for every use case
- Free and open source — backed by Red Hat
Install Ansible
# Ubuntu/Debian
sudo apt update
sudo apt install ansible -y
# RHEL/CentOS/Fedora
sudo dnf install ansible-core -y
# macOS
brew install ansible
# pip (any OS)
pip install ansible
# Verify
ansible --version
Core Concepts
┌─────────────────────────────────────────┐
│ PLAYBOOK │
│ (YAML file describing desired state) │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Play 1 │ │ Play 2 │ │
│ │ hosts: │ │ hosts: │ │
│ │ web │ │ db │ │
│ │ │ │ │ │
│ │ Task 1 │ │ Task 1 │ │
│ │ Task 2 │ │ Task 2 │ │
│ └─────────┘ └─────────┘ │
└──────────────────┬──────────────────────┘
│
┌───────┴───────┐
│ INVENTORY │
│ (host list) │
│ │
│ web-01 │
│ web-02 │
│ db-01 │
└───────────────┘
| Concept | Description |
|---|---|
| Control Node | Machine where Ansible runs (your laptop/CI server) |
| Managed Node | Remote server being managed |
| Inventory | List of managed hosts |
| Playbook | YAML file with automation tasks |
| Play | Maps hosts to tasks |
| Task | Single action (install package, copy file, etc.) |
| Module | Unit of code Ansible executes (apt, copy, service) |
| Role | Reusable bundle of tasks, templates, files |
| Handler | Task triggered by notifications (e.g., restart service) |
Step 1: Create Inventory
# inventory.ini
[webservers]
web-01 ansible_host=192.168.1.10
web-02 ansible_host=192.168.1.11
[dbservers]
db-01 ansible_host=192.168.1.20
[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/id_ed25519
Or YAML format:
# inventory.yml
all:
children:
webservers:
hosts:
web-01:
ansible_host: 192.168.1.10
web-02:
ansible_host: 192.168.1.11
dbservers:
hosts:
db-01:
ansible_host: 192.168.1.20
vars:
ansible_user: deploy
Step 2: Test Connection
# Ping all hosts
ansible -i inventory.ini all -m ping
# Expected output:
# web-01 | SUCCESS => { "ping": "pong" }
# web-02 | SUCCESS => { "ping": "pong" }
# db-01 | SUCCESS => { "ping": "pong" }
# Run ad-hoc command
ansible -i inventory.ini webservers -m command -a "uptime"
Step 3: Write Your First Playbook
# playbook.yml
---
- name: Setup web servers
hosts: webservers
become: true # Run as root (sudo)
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Copy website
ansible.builtin.copy:
src: files/index.html
dest: /var/www/html/index.html
mode: "0644"
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
# Run it
ansible-playbook -i inventory.ini playbook.yml
Step 4: Variables
---
- name: Deploy application
hosts: webservers
become: true
vars:
app_name: myapp
app_port: 8080
app_user: deploy
tasks:
- name: Create app user
ansible.builtin.user:
name: "{{ app_user }}"
shell: /bin/bash
- name: Create app directory
ansible.builtin.file:
path: "/opt/{{ app_name }}"
state: directory
owner: "{{ app_user }}"
mode: "0755"
- name: Show config
ansible.builtin.debug:
msg: "{{ app_name }} will run on port {{ app_port }}"
Step 5: Templates
- name: Deploy nginx config
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/myapp
notify: Reload nginx
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
# templates/nginx.conf.j2
server {
listen {{ app_port }};
server_name {{ domain }};
location / {
proxy_pass http://127.0.0.1:{{ backend_port }};
}
}
Step 6: Conditionals and Loops
# Conditionals
- name: Install on Debian
ansible.builtin.apt:
name: nginx
when: ansible_os_family == "Debian"
- name: Install on RHEL
ansible.builtin.yum:
name: nginx
when: ansible_os_family == "RedHat"
# Loops
- name: Create users
ansible.builtin.user:
name: "{{ item }}"
state: present
loop:
- alice
- bob
- carol
# Loop with dict
- name: Install packages
ansible.builtin.apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
loop:
- { name: nginx, state: present }
- { name: apache2, state: absent }
Step 7: Roles
# Create role structure
ansible-galaxy init roles/webserver
roles/webserver/
├── defaults/main.yml # Default variables
├── handlers/main.yml # Handlers
├── tasks/main.yml # Tasks
├── templates/ # Jinja2 templates
├── files/ # Static files
└── meta/main.yml # Dependencies
# roles/webserver/tasks/main.yml
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx
# Use the role
---
- hosts: webservers
become: true
roles:
- webserver
Step 8: Ansible Configuration
# ansible.cfg (in project root)
[defaults]
inventory = inventory.yml
remote_user = deploy
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
[privilege_escalation]
become = True
become_method = sudo
Complete Example: Deploy a Web App
---
- name: Deploy web application
hosts: webservers
become: true
vars:
app_version: "2.1.0"
app_user: deploy
app_dir: /opt/myapp
app_port: 8080
tasks:
- name: Install dependencies
ansible.builtin.apt:
name: [nginx, python3, python3-pip]
state: present
update_cache: true
- name: Create app user
ansible.builtin.user:
name: "{{ app_user }}"
system: true
shell: /usr/sbin/nologin
- name: Create app directory
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
- name: Deploy application
ansible.builtin.unarchive:
src: "https://releases.example.com/myapp-{{ app_version }}.tar.gz"
dest: "{{ app_dir }}"
remote_src: true
owner: "{{ app_user }}"
notify: Restart app
- name: Deploy systemd service
ansible.builtin.template:
src: myapp.service.j2
dest: /etc/systemd/system/myapp.service
notify: Restart app
- name: Deploy nginx reverse proxy
ansible.builtin.template:
src: nginx-proxy.conf.j2
dest: /etc/nginx/sites-enabled/myapp
notify: Reload nginx
- name: Start services
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop: [nginx, myapp]
handlers:
- name: Restart app
ansible.builtin.systemd:
name: myapp
state: restarted
daemon_reload: true
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Next Steps
| Topic | Article |
|---|---|
| Playbooks in depth | Ansible Playbook Guide |
| Organize with roles | Ansible Roles Guide |
| Manage secrets | Ansible Vault Guide |
| Template files | Ansible Jinja2 Templates |
| Test your roles | Ansible Molecule Testing |
| Best practices | Ansible Best Practices |
| Performance | Speed Up Ansible |
| Windows | Ansible Windows Guide |
| Cloud | Ansible AWS Guide |
| Errors | Ansible Troubleshooting |
Conclusion
You've learned the fundamentals: inventory, playbooks, variables, templates, conditionals, loops, roles, and handlers. Ansible's power comes from its simplicity — YAML files describing what you want, and modules that make it happen idempotently. Start small (one server, one task), build up to roles, then scale to your entire infrastructure. The 3,000+ built-in modules mean there's almost nothing Ansible can't automate.