When you manage multiple VPS servers, SSH-ing into each one individually is neither scalable nor repeatable. Ansible is a Configuration Management tool that lets you write a "recipe" once and run it across all servers simultaneously over SSH — without installing any agent on the target machines.
What is Ansible and Why Use It?
Ansible is Red Hat's open-source IT automation tool. It uses YAML to write Playbooks — declarative files describing what state a system should be in (e.g., Nginx must be installed, this config file must exist, this user must be present). Ansible makes the system match the declared state in an idempotent way — running the same Playbook multiple times always produces the same result.
- Agentless — communicates via SSH only, nothing to install on managed nodes
- YAML-based — readable, writable without deep programming knowledge
- Idempotent — safe to re-run; no unintended side effects
- Large ecosystem — 3,000+ built-in modules covering virtually every task
Prerequisites
- Control Node — the machine that runs Ansible (macOS, Linux, or a management VPS). Requires Python 3.8+
- Managed Nodes — target VPS servers (Ubuntu 20.04/22.04/24.04 or Debian). Python 3 must be present
- SSH Key — your public key must be in
~/.ssh/authorized_keyson all managed nodes
Install Ansible on the Control Node
Ubuntu / Debian
sudo apt update
sudo apt install -y ansible
macOS (Homebrew)
brew install ansible
Python pip (any OS)
pip3 install ansible
Verify the installation:
ansible --version
# ansible [core 2.17.x]
Create an Inventory File
The inventory tells Ansible which servers exist and how to connect to them.
inventory.ini (basic format)
[webservers]
web1 ansible_host=203.0.113.10 ansible_user=root
web2 ansible_host=203.0.113.11 ansible_user=root
[databases]
db1 ansible_host=203.0.113.20 ansible_user=root
[all:vars]
ansible_ssh_private_key_file=~/.ssh/id_ed25519
Test Connectivity
ansible -i inventory.ini all -m ping
A pong response from every host confirms the connection is working.
Write Your First Playbook — Install Nginx
Create setup-nginx.yml:
---
- name: Install and enable Nginx
hosts: webservers
become: yes
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install Nginx
apt:
name: nginx
state: present
- name: Start Nginx and enable on boot
service:
name: nginx
state: started
enabled: yes
- name: Allow port 80 in UFW
ufw:
rule: allow
port: '80'
proto: tcp
Run the playbook:
ansible-playbook -i inventory.ini setup-nginx.yml
Idempotency in practice: Run this playbook 10 times and Nginx will only be installed once. Ansible checks the actual state first — if a package already exists, the task shows "ok" instead of "changed" and no action is taken.
Variables — Making Playbooks Flexible
---
- name: Install Web Stack
hosts: webservers
become: yes
vars:
php_version: "8.3"
app_user: "www-data"
tasks:
- name: Install PHP {{ php_version }}
apt:
name: "php{{ php_version }}-fpm"
state: present
Separate Variables into Files
# vars/main.yml
php_version: "8.3"
mysql_root_password: "SecurePass123"
app_domain: "example.com"
# In playbook
vars_files:
- vars/main.yml
Handlers — Run Tasks Only When Changes Occur
Handlers are special tasks that only run when "notified" by another task — perfect for restarting services after a config change.
tasks:
- name: Copy Nginx config
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart Nginx
handlers:
- name: Restart Nginx
service:
name: nginx
state: restarted
If the config hasn't changed, Nginx won't be restarted — reducing unnecessary downtime.
Roles — Organizing Large Playbooks
ansible-galaxy init roles/nginx
# Creates structure:
# roles/nginx/
# tasks/main.yml
# handlers/main.yml
# templates/
# vars/main.yml
# defaults/main.yml
New VPS Hardening Playbook
---
- name: Setup new VPS
hosts: new_servers
become: yes
tasks:
- name: Full system upgrade
apt:
upgrade: dist
update_cache: yes
- name: Install essential packages
apt:
name:
- ufw
- fail2ban
- unattended-upgrades
- curl
- git
state: present
- name: Create deploy user
user:
name: deploy
shell: /bin/bash
groups: sudo
append: yes
- name: Add SSH public key
authorized_key:
user: deploy
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
- name: Disable root SSH login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
notify: Restart SSH
- name: Enable UFW
ufw:
state: enabled
policy: deny
- name: Allow SSH port
ufw:
rule: allow
port: '22'
proto: tcp
handlers:
- name: Restart SSH
service:
name: ssh
state: restarted
Ad-hoc Commands
# Check disk usage on all servers
ansible -i inventory.ini all -m shell -a "df -h /"
# Restart Nginx on webservers
ansible -i inventory.ini webservers -m service -a "name=nginx state=restarted" --become
# Copy a file to all servers
ansible -i inventory.ini all -m copy -a "src=app.conf dest=/etc/app.conf"
Ansible Galaxy: Community-maintained roles are available at galaxy.ansible.com. Install with ansible-galaxy install geerlingguy.mysql and use directly in your playbooks.
Practical Tips
- Use
--check(dry run) to preview changes before applying:ansible-playbook --check setup.yml - Add
--diffto see exactly what files will change - Store inventory and playbooks in Git for version control and team collaboration
- Use
ansible-vault encrypt vars/secrets.ymlto encrypt passwords before committing - Name tasks descriptively — readable logs make troubleshooting much easier
Ansible Vault — Encrypting Secrets Safely
When your Playbooks contain passwords, API keys, or other credentials, use Ansible Vault to encrypt the files before committing them to version control. This keeps secrets out of your repository while still allowing Ansible to access them at runtime.
Create and Encrypt a Variables File
# Create an encrypted secrets file
ansible-vault create vars/secrets.yml
# Edit an already-encrypted file
ansible-vault edit vars/secrets.yml
# Encrypt an existing plain-text file
ansible-vault encrypt vars/secrets.yml
# Temporarily decrypt to inspect content
ansible-vault decrypt vars/secrets.yml
Reference the Encrypted File in a Playbook
vars_files:
- vars/main.yml
- vars/secrets.yml # encrypted file — Ansible decrypts at runtime
# Run with interactive vault password prompt
ansible-playbook -i inventory.ini deploy.yml --ask-vault-pass
# Run with a password file (suitable for CI/CD pipelines)
ansible-playbook -i inventory.ini deploy.yml --vault-password-file ~/.vault_pass
Best practice: Commit the encrypted vars/secrets.yml file to Git — the encryption makes it safe to store in version control. Never add secrets.yml to .gitignore unencrypted, as you might accidentally commit a plain-text version later.
Ansible in a CI/CD Pipeline
Integrating Ansible with a CI/CD tool lets you automatically deploy to all your VPS servers whenever you push code — without any manual steps.
Example GitHub Actions Workflow
# .github/workflows/deploy.yml
name: Deploy with Ansible
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: pip3 install ansible
- name: Write SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
- name: Run Ansible Playbook
run: |
ansible-playbook -i inventory.ini deploy.yml \
--vault-password-file <(echo "${{ secrets.VAULT_PASS }}")
With this workflow, every merge into main triggers an automatic deployment across all servers in your inventory — no manual SSH required.
| CI/CD Tool | Ansible Integration | Complexity |
|---|---|---|
| GitHub Actions | pip install ansible in runner | Low |
| GitLab CI | Docker image with Ansible pre-installed | Low |
| Jenkins | Ansible plugin or shell step | Medium |
| AWX / Ansible Tower | Built-in — purpose-built integration | High (enterprise) |
Troubleshooting Common Issues
These are the most frequently encountered problems when getting started with Ansible, along with their solutions.
SSH Connection Refused
# Debug with maximum verbosity to see exact SSH commands
ansible -i inventory.ini all -m ping -vvv
# Add host fingerprint to known_hosts before first run
ssh-keyscan -H 203.0.113.10 >> ~/.ssh/known_hosts
Permission Denied When Running Tasks
# Tasks requiring root must include become: yes
- name: Install package
apt:
name: nginx
state: present
become: yes # required for privileged operations
# Or set become at the play level for all tasks
- hosts: webservers
become: yes
Python Not Detected on Managed Node
# Specify the Python interpreter explicitly in inventory
web1 ansible_host=203.0.113.10 ansible_python_interpreter=/usr/bin/python3
# Or set it globally in group vars
[all:vars]
ansible_python_interpreter=/usr/bin/python3
Debugging tip: Add -v, -vv, or -vvv to any Ansible command for increasing verbosity. At -vvv you can see the exact SSH command Ansible is running — invaluable for diagnosing connection problems.
Ansible vs Other Configuration Management Tools
Several tools compete in the configuration management space. Understanding their trade-offs helps you choose the right tool for your infrastructure scale and team skill set.
| Tool | Agent Required | Config Language | Best For |
|---|---|---|---|
| Ansible | No (agentless) | YAML | Any scale, easy onboarding |
| Puppet | Yes | DSL | Large enterprise fleets |
| Chef | Yes | Ruby DSL | Ruby-familiar teams |
| Terraform | No | HCL | Cloud infrastructure provisioning |
Ansible is the most practical choice for managing VPS servers when your priority is simplicity, no-agent overhead, and readable YAML configuration. It works over the same SSH connection you already use for server access, making adoption gradual and low-risk for teams of any size.
Need a VPS for Running Ansible?
AsiaGB offers Linux VPS (Ubuntu/Debian) with Full Root Access starting at ฿500/month.
View VPS Plans