Why Ansible?
In self-hosting and VPS administration, manual SSH login to each machine becomes unsustainable as server count grows. Ansible provides an elegant solution — no agents required on managed nodes, just SSH connectivity to execute configurations and deploy applications across any scale.
Compared to competitors like Puppet, Chef, and SaltStack, Ansible’s key advantages are:
- Agentless architecture: Managed nodes only need SSH and Python — no extra daemons
- Declarative syntax: Playbooks use YAML, highly readable and beginner-friendly
- Idempotency guarantee: Running the same Playbook repeatedly produces no side effects
- Rich module ecosystem: 6000+ built-in modules cover nearly every common ops scenario
- Gradual adoption: Start with a single server, scale to clusters seamlessly
I. Getting Started with Ansible
1.1 Installing Ansible
Ansible supports all major Linux distributions, macOS, and Windows (via WSL).
# Ubuntu/Debian
sudo apt update
sudo apt install -y ansible
# CentOS/RHEL/Fedora
sudo dnf install -y ansible
# macOS (Homebrew)
brew install ansible
# Using pip (recommended for latest version)
pip install ansible-core
Verify installation:
ansible --version
1.2 Configuring the Inventory File
The inventory file defines all hosts you want to manage. Create inventory.ini:
# Group hosts by role
[webservers]
web1.example.com ansible_host=192.168.1.10
web2.example.com ansible_host=192.168.1.11
web3.example.com ansible_host=192.168.1.12
[dbservers]
db1.example.com ansible_host=192.168.1.20
[monitoring]
monitor1.example.com ansible_host=192.168.1.30
# Group variables: shared config for all web servers
[webservers:vars]
http_port=80
nginx_version=1.24
# All hosts variable
[all:vars]
ansible_user=admin
ansible_ssh_private_key_file=~/.ssh/id_ed25519
ansible_python_interpreter=/usr/bin/python3
1.3 First Connectivity Test
# Test connectivity to all hosts
ansible all -i inventory.ini -m ping
# Test specific group
ansible webservers -i inventory.ini -m ping
# List hosts in a group
ansible webservers --list-hosts -i inventory.ini
Expected output:
web1.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
web2.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
II. Playbook Core Concepts
A Playbook is Ansible’s configuration description file in YAML format. A typical Playbook structure:
---
- name: Configure Web Servers
hosts: webservers
become: true # Execute with sudo
gather_facts: true # Collect target host facts
vars:
nginx_version: "1.24"
app_port: 8080
tasks:
# Task list...
handlers:
# Handlers (triggered by tasks)...
2.1 Common Modules Quick Reference
| Module | Purpose | Example |
|---|---|---|
apt / yum / dnf | Package management | apt: name=nginx state=present |
service / systemd | Service management | systemd: name=nginx state=restarted |
copy | File copy | copy: src=nginx.conf dest=/etc/nginx/ |
template | Jinja2 templates | template: src=app.conf.j2 dest=/etc/app/ |
git | Code checkout | git: repo=https://... dest=/opt/app |
user | User management | user: name=deploy groups=sudo |
file | File operations | file: path=/data mode=0755 state=directory |
lineinfile | Line editing in files | lineinfile: path=/etc/hosts line="10.0.0.1 db" |
shell / command | Execute commands | shell: docker ps --format json |
debug | Debug output | debug: msg="Value is {{ my_var }}" |
2.2 Writing Your First Playbook
Create a basic system hardening Playbook:
---
- name: VPS Basic Security Hardening
hosts: all
become: true
vars:
ntp_server: "pool.ntp.org"
fail2ban_enabled: true
tasks:
- name: Update system packages
apt:
update_cache: true
upgrade: dist
tags: ['update']
- name: Install essential tools
apt:
name:
- curl
- git
- htop
- fail2ban
- ufu
state: present
tags: ['packages']
- name: Configure firewall rules
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: [22, 80, 443]
tags: ['firewall']
- name: Disable root remote login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
notify: Restart SSH service
tags: ['ssh']
- name: Start and enable fail2ban
systemd:
name: fail2ban
state: started
enabled: true
tags: ['fail2ban']
handlers:
- name: Restart SSH service
systemd:
name: sshd
state: restarted
Run the Playbook:
ansible-playbook -i inventory.ini harden.yml --check --diff
ansible-playbook -i inventory.ini harden.yml
--check mode (dry run) makes no actual system changes; --diff shows specific content changes.
III. Advanced Techniques and Best Practices
3.1 Conditional Logic and Loops
- name: Install packages based on OS type
package:
name: "{{ item }}"
state: present
loop:
- curl
- git
when: ansible_os_family == "Debian"
- name: Create multiple users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
loop: "{{ users_list }}"
3.2 Jinja2 Templates
Template file templates/nginx.conf.j2:
server {
listen {{ http_port }};
server_name {{ inventory_hostname }};
root {{ app_root }};
index index.html;
{% if ssl_enabled %}
ssl_certificate /etc/ssl/certs/{{ domain }}.crt;
ssl_certificate_key /etc/ssl/private/{{ domain }}.key;
{% endif %}
location / {
proxy_pass http://127.0.0.1:{{ app_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Reference in Playbook:
- name: Deploy Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
mode: '0644'
notify: Reload Nginx
3.3 Roles for Code Reuse
Roles are Ansible’s core code reuse mechanism. Directory structure:
roles/
└── webserver/
├── tasks/
│ ├── main.yml
│ └── nginx.yml
├── handlers/
│ └── main.yml
├── templates/
│ └── nginx.conf.j2
├── vars/
│ └── main.yml
├── defaults/
│ └── main.yml
└── meta/
└── main.yml
roles/webserver/tasks/main.yml:
---
- name: Install Nginx
import_tasks: nginx.yml
- name: Deploy application
copy:
src: app/
dest: /var/www/app
Use roles in Playbook:
---
- name: Deploy Web Service
hosts: webservers
roles:
- webserver
vars:
app_version: "2.1.0"
3.4 Variable Priority
Ansible variables follow this priority order (low to high):
- Role defaults (lowest)
- Inventory variables
- Play vars
- Host facts (gather_facts)
- Role vars
- Task vars
- Extra vars (
-eCLI arguments, highest)
IV. Batch Deployment in Practice
4.1 One-Click Docker Compose Stack Deployment
---
- name: Deploy Docker Compose Application Stack
hosts: webservers
become: true
vars:
compose_dir: /opt/apps/{{ app_name }}
apps:
- name: traefik
stack: traefik
- name: monitoring
stack: monitoring
tasks:
- name: Ensure Docker is installed
ansible.builtin.package:
name: docker.io
state: present
- name: Add user to docker group
user:
name: "{{ ansible_user }}"
groups: docker
append: true
- name: Create application directory
file:
path: "{{ compose_dir }}"
state: directory
mode: '0755'
- name: Deploy docker-compose files
template:
src: "{{ item.stack }}.yml.j2"
dest: "{{ compose_dir }}/docker-compose.yml"
loop: "{{ apps }}"
- name: Start services
community.docker.docker_compose_v2:
project_src: "{{ compose_dir }}"
state: present
pull: always
4.2 Batch System Upgrade with Controlled Reboot
---
- name: Batch system upgrade with safe reboot
hosts: all
serial: 3 # Process only 3 hosts at a time
gather_facts: false
tasks:
- name: Upgrade system packages
apt:
upgrade: dist
update_cache: true
when: ansible_distribution == "Ubuntu"
- name: Check if reboot is needed
command: needs-restarting -r
register: needs_reboot
changed_when: false
failed_when: false
- name: Reboot hosts that need it
reboot:
reboot_timeout: 300
when: needs_reboot.rc == 1
serial: 3 ensures only 3 servers are upgraded at a time, preventing total service outage.
4.3 Configuration Drift Detection and Remediation
---
- name: Detect and fix configuration drift
hosts: all
become: true
tasks:
- name: Ensure SSH configuration is correct
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
loop:
- { regexp: '^PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^X11Forwarding', line: 'X11Forwarding no' }
- name: Ensure critical file permissions are correct
file:
path: "{{ item.path }}"
mode: "{{ item.mode }}"
loop:
- { path: /etc/ssh/sshd_config, mode: '0600' }
- { path: /etc/ssh/ssh_known_hosts, mode: '0644' }
- { path: /etc/crontab, mode: '0600' }
V. Performance Optimization and Large-Scale Deployment
5.1 Connection Optimization
# ansible.cfg
[defaults]
forks = 50 # Parallelize 50 tasks
timeout = 30
retry_files_enabled = false
inventory = ./inventory.ini
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
pipelining = true # SSH pipelining, significant performance boost
5.2 Sharded Execution
For very large clusters, use --limit and --tags:
# Execute only specific tagged tasks
ansible-playbook deploy.yml --tags 'nginx,ssl'
# Limit to specific hosts
ansible-playbook deploy.yml --limit 'web[1:5]'
# Skip specific tags
ansible-playbook deploy.yml --skip-tags 'database'
5.3 Ansible Vault for Sensitive Data
# Create encrypted variable file
ansible-vault create group_vars/all/secrets.yml
# Edit existing encrypted file
ansible-vault edit group_vars/all/secrets.yml
# Decrypt at runtime
ansible-playbook deploy.yml --ask-vault-pass
Encrypted file example:
# group_vars/all/secrets.yml (stored encrypted)
db_password: "s3cur3P@ssw0rd"
api_key: "ak_live_xxxxxxxx"
ssl_cert_key: "{{ vault_ssl_key }}"
VI. Troubleshooting
6.1 Connection Failures
# Verbose debug output
ansible all -m ping -vvv
# Test SSH connection
ansible all -m debug -a "msg='SSH test'" -vvv
# Check Python path
ansible all -m setup -a "filter=ansible_python*"
Common issues and solutions:
| Symptom | Cause | Solution |
|---|---|---|
SSH: hostname could not be resolved | DNS issue | Check /etc/hosts or use ansible_host |
Permission denied (publickey) | SSH key issue | Confirm ansible_ssh_private_key_file path is correct |
Python not found | Missing Python | Specify ansible_python_interpreter path |
MODULE FAILURE | Missing module dependency | Install required dependency on managed node |
6.2 Idempotency Issues
# ❌ Non-idempotent: appends every time
- shell: echo "new_line" >> /etc/config
# ✅ Idempotent: uses lineinfile
- lineinfile:
path: /etc/config
line: "new_line"
state: present
VII. Summary
Ansible is an indispensable automation tool for self-hosted VPS operations. Through this guide, you’ve learned:
- Basics: Installation, inventory configuration, connectivity testing
- Core: Playbook authoring, module usage, conditionals and loops
- Advanced: Role reuse, templating, variable management
- Practice: Batch deployment, upgrade control, configuration drift remediation
- Optimization: Connection tuning, sharded execution, Vault encryption
- Troubleshooting: Common issue diagnosis and resolution
From a single VPS to a cluster of hundreds, Ansible delivers consistent, repeatable, auditable configuration management. Remember the key principles: declarative over imperative, idempotency is critical, role-based for reuse.
Get started now — run ansible all -m ping on your first VPS, and your automation journey begins.
