Introduction: Why You Need Ansible
Imagine you have 10 VPS instances, each requiring Docker installation, Nginx reverse proxy setup, SSL certificate configuration, and regular security patching. Doing this manually could take hours or even days.
Ansible lets you write a single configuration file and execute the same operations across all your VPS instances — this is the core philosophy of Infrastructure as Code (IaC).
Manual Operations → Error-prone, time-consuming, unreproducible
Ansible Operations → One-click deployment, version-controlled, auditable
What Is Ansible?
Ansible is an open-source automation tool maintained by Red Hat, featuring:
- Agentless Architecture: No agent required on target servers — connects via SSH
- Declarative Configuration: Describe the desired end state; Ansible handles the differences
- YAML Syntax: Human-readable configuration language with a gentle learning curve
- Rich Module Library: Over 3,000 built-in modules covering virtually every ops scenario
Environment Prerequisites
Control Node Requirements
Ansible runs on a Control Node, which can be your local machine or any VPS:
| Component | Recommended Config |
|---|---|
| OS | Ubuntu 22.04+ / Debian 12+ / CentOS Stream 9 |
| Python | Python 3.8+ |
| Memory | 2GB+ |
| Disk | 10GB+ (for playbooks and inventory) |
Managed Node Requirements
Target VPS only need:
- Linux: Python 3 or Python 2.7 + Jinja2
- Windows: WinRM + PowerShell (optional)
- Network: Control node must reach all managed nodes via SSH
Step 1: Install Ansible
# Ubuntu/Debian
sudo apt update
sudo apt install -y python3-pip sshpass
pip3 install --user ansible
# CentOS/RHEL
sudo dnf install -y epel-release
sudo dnf install -y ansible sshpass
# Verify installation
ansible --version
Step 2: Configure SSH Key-Based Authentication
This is critical for Ansible’s efficiency. Generate SSH keys on the control node and distribute them:
# 1. Generate key pair (if you don't have one)
ssh-keygen -t ed25519 -C "ansible@control-node" -N ""
# 2. Distribute public key to all VPS instances
ssh-copy-id -o StrictHostKeyChecking=no user@vps1.example.com
ssh-copy-id -o StrictHostKeyChecking=no user@vps2.example.com
ssh-copy-id -o StrictHostKeyChecking=no user@vps3.example.com
# 3. Test connectivity
ansible all -m ping
Output SUCCESS means everything is configured correctly!
Step 3: Create Inventory File
The inventory file defines which hosts Ansible manages and how they’re grouped:
# inventory.ini
[webservers]
vps1.example.com ansible_host=1.2.3.4
vps2.example.com ansible_host=5.6.7.8
[databases]
db1.example.com ansible_host=9.10.11.12
[monitoring]
mon1.example.com ansible_host=13.14.15.16
[all:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3
You can group by business logic:
[production]
web-prod-1.example.com
web-prod-2.example.com
db-prod-1.example.com
[staging]
web-stg-1.example.com
db-stg-1.example.com
[production:children]
webservers
databases
Step 4: Write Your First Playbook
A Playbook is the heart of Ansible — an automation script written in YAML. Here’s a complete example:
Basic System Hardening Playbook
# playbook.yml
---
- name: Basic System Hardening
hosts: all
become: true
vars:
timezone: Asia/Shanghai
ntp_servers:
- 0.cn.pool.ntp.org
- 1.cn.pool.ntp.org
firewall_allowed_ports:
- 22/tcp
- 80/tcp
- 443/tcp
tasks:
- name: Update system packages
apt:
update_cache: yes
upgrade: dist
when: ansible_os_family == "Debian"
- name: Install common tools
apt:
name:
- curl
- wget
- htop
- jq
- vim
- git
- unzip
state: present
- name: Configure timezone
timezone:
timezone: "{{ timezone }}"
- name: Configure NTP time sync
systemd:
name: chrony
state: started
enabled: yes
when: ansible_os_family == "RedHat"
- name: Create admin user
user:
name: admin
groups: sudo
shell: /bin/bash
state: present
- name: Harden SSH configuration
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" }
notify: Restart SSH
- name: Configure firewall (UFW)
ufw:
rule: allow
port: "{{ item }}"
loop: "{{ firewall_allowed_ports }}"
handlers:
- name: Restart SSH
service:
name: sshd
state: restarted
Running the Playbook
# Syntax check (dry-run)
ansible-playbook playbook.yml --check --diff
# Execute
ansible-playbook playbook.yml -i inventory.ini -v
# Target specific group
ansible-playbook playbook.yml -i inventory.ini --limit webservers
Step 5: Docker Container Deployment
Most modern VPS run Docker containers. This Playbook automates the entire process:
# docker-setup.yml
---
- name: Install and Configure Docker
hosts: all
become: true
tasks:
- name: Install Docker dependencies
apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
- name: Add Docker GPG key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Add Docker repository
apt_repository:
repo: >-
deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg]
https://download.docker.com/linux/ubuntu
{{ ansible_lsb.codename | lower }} stable
state: present
- name: Install Docker Engine
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: yes
- name: Start Docker service
systemd:
name: docker
state: started
enabled: yes
- name: Configure Docker mirror
copy:
content: |
{
"registry-mirrors": ["https://mirror.ccs.tencentyun.com"]
}
dest: /etc/docker/daemon.json
notify: Restart Docker
- name: Allow current user to manage Docker
user:
name: "{{ ansible_user }}"
groups: docker
append: yes
handlers:
- name: Restart Docker
systemd:
name: docker
state: restarted
Step 6: Nginx Reverse Proxy Template
Use Ansible’s templating to dynamically generate Nginx configs based on variables:
# templates/nginx-site.conf.j2
server {
listen 80;
server_name {{ domain }};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://{{ domain }}$request_uri;
}
}
server {
listen 443 ssl http2;
server_name {{ domain }};
ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
{% if enable_gzip %}
gzip on;
gzip_types text/plain application/json application/javascript text/css;
{% endif %}
location / {
proxy_pass http://{{ backend_service }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Corresponding Playbook task:
- name: Deploy Nginx site config
template:
src: nginx-site.conf.j2
dest: /etc/nginx/sites-available/{{ domain }}
owner: root
group: root
mode: '0644'
notify: Reload Nginx
- name: Enable site
file:
src: /etc/nginx/sites-available/{{ domain }}
dest: /etc/nginx/sites-enabled/{{ domain }}
state: link
notify: Reload Nginx
Step 7: Automatic SSL Certificate Renewal
Let’s Encrypt certificates are valid for only 90 days. Ansible can automate renewal:
# certbot-setup.yml
---
- name: Install and Configure Certbot
hosts: all
become: true
tasks:
- name: Install Certbot
apt:
name: certbot
state: present
- name: Request SSL certificate
community.general.certbot:
command: certificate_only
email: admin@{{ domain }}
domains:
- "{{ domain }}"
- "www.{{ domain }}"
challenge_hook: "systemctl reload nginx"
account_sid: my-account
state: present
- name: Create renewal script
copy:
content: |
#!/bin/bash
certbot renew --quiet --post-hook "systemctl reload nginx"
logger "Certificate renewal completed"
dest: /usr/local/bin/renew-certs.sh
mode: '0755'
- name: Schedule cron job (daily at 3 AM)
cron:
name: "Auto-renew SSL certificates"
minute: "0"
hour: "3"
job: "/usr/local/bin/renew-certs.sh >> /var/log/certbot-renew.log 2>&1"
Step 8: Multi-Environment Management
Different configurations for production and development environments:
project/
├── inventory/
│ ├── production.ini
│ ├── staging.ini
│ └── development.ini
├── group_vars/
│ ├── all.yml # Global variables
│ ├── webservers.yml # Web server variables
│ └── databases.yml # Database variables
├── host_vars/
│ ├── vps1.example.com.yml
│ └── db1.example.com.yml
├── roles/
│ ├── nginx/
│ ├── docker/
│ ├── certbot/
│ └── monitoring/
└── site.yml # Main Playbook
Variable Priority
# group_vars/all.yml
ntp_servers:
- 0.cn.pool.ntp.org
- 1.cn.pool.ntp.org
# host_vars/vps1.example.com.yml (overrides global)
ntp_servers:
- ntp1.mycompany.com
- ntp2.mycompany.com
Step 9: Monitoring Setup
Deploy Uptime Kuma or Prometheus Node Exporter on each VPS:
# monitoring-setup.yml
---
- name: Deploy monitoring agents
hosts: all
become: true
tasks:
- name: Deploy Node Exporter
docker_container:
name: node-exporter
image: prom/node-exporter:latest
restart_policy: always
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
- name: Deploy Uptime Kuma
docker_container:
name: uptime-kuma
image: louislam/uptime-kuma:1
restart_policy: always
ports:
- "3001:3001"
volumes:
- uptime-kuma-data:/app/data
Step 10: CI/CD Integration
Integrate Ansible with GitHub Actions for automatic deployments:
# .github/workflows/deploy.yml
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: pip3 install ansible
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts
- name: Run Playbook
run: |
ansible-playbook site.yml \
-i inventory/production.ini \
-e "deploy_version=${{ github.sha }}" \
-v
Best Practices & Pitfalls
✅ Do’s
- Organize code with Roles: Encapsulate each function as an independent role for reusability
- Dry-run first: Always use
--check --diffto preview changes - Version control Playbooks: Manage infrastructure like you manage code
- Principle of least privilege: Use sudo users instead of root
- Use Ansible Vault for sensitive data: Never store passwords or API keys in plain text
# Encrypt sensitive variables
ansible-vault encrypt group_vars/secrets.yml
ansible-vault edit group_vars/secrets.yml # Edit encrypted file
# Decrypt at runtime
ansible-playbook site.yml --ask-vault-pass
❌ Don’ts
- Avoid overusing the
rawmodule: Prefer standard modules - Don’t hardcode IP addresses in Playbooks: Use Inventory for host management
- Don’t ignore error handling: Be cautious with
ignore_errors - Don’t modify too many hosts at once: Test on a single machine first
- Don’t skip documentation: Every Playbook should have clear comments
Cost Comparison: Manual vs Ansible
| Metric | Manual Operations | Ansible Automation |
|---|---|---|
| Initial setup (10 VPS) | ~8 hours | ~30 minutes |
| Bulk security patches | ~4 hours | ~5 minutes |
| New server provisioning | ~2 hours | ~10 minutes |
| Error probability | High (human error) | Extremely low (consistent execution) |
| Audit trail | None | Complete log records |
| Monthly time saved | - | 20+ hours |
Advanced: Ansible AWX / Tower
When managing more than 50 VPS instances, consider AWX (the open-source enterprise web UI for Ansible):
# awx-deploy.yml
---
- name: Deploy AWX Management Panel
hosts: localhost
connection: local
tasks:
- name: Clone AWX repository
git:
repo: https://github.com/ansible/awx.git
dest: /opt/awx
version: latest
- name: Build AWX
shell: make deploy
args:
chdir: /opt/awx
AWX provides:
- Web GUI to manage all Playbooks
- Visual Inventory and host grouping
- Scheduled tasks and scheduler
- User permission management
- Execution history and audit logs
Conclusion
Ansible is the Swiss Army knife of VPS operations — write once, execute everywhere. From single-server hardening to multi-machine clusters, from container orchestration to certificate management, Ansible handles it all.
Action Plan:
- Install Ansible on your first VPS today
- Write Playbooks for your most repetitive tasks
- Gradually add other VPS to your management scope
- Use Vault to protect sensitive information
- Push Playbooks to Git for version control
Remember: The best automation starts by solving one pain point. Don’t try to automate everything at once — start small and expand gradually.
Enjoyed this article? Follow selfvps.net for more self-hosting and VPS cost-saving tips!
