Ansible automated VPS management

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.

Prerequisites

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

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 Actionspip install ansible in runnerLow
GitLab CIDocker image with Ansible pre-installedLow
JenkinsAnsible plugin or shell stepMedium
AWX / Ansible TowerBuilt-in — purpose-built integrationHigh (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
AnsibleNo (agentless)YAMLAny scale, easy onboarding
PuppetYesDSLLarge enterprise fleets
ChefYesRuby DSLRuby-familiar teams
TerraformNoHCLCloud 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

View all affordable VPS Thailand plans →