Managing a handful of VPS instances by hand — SSHing into each one to install packages, push config files, and run updates — works until the fleet grows past three or four boxes. Then every change becomes a sequence of copy-paste sessions, and drift creeps in: one server runs Nginx 1.24, another 1.26, and nobody remembers which one has the patched sshd config. Ansible solves this by describing the desired state of every server in plain YAML files and applying them over SSH, with no agent installed on the targets. This guide covers the practical core: inventory, ad-hoc commands, playbooks, and automating security maintenance.
Ansible is agentless — it connects to each host over SSH, executes tasks, and disconnects. That makes it a natural fit for VPS fleets, because there is no separate daemon to keep updated and no open management port beyond the SSH you already hardened. If you are still deciding where to run your fleet, our comparison table helps you shortlist providers with consistent CPU and network specs, which keeps playbook performance predictable across nodes.
Setting Up the Control Node and Inventory
Install Ansible on any machine that can reach your servers — a laptop or a small admin VPS both work. The inventory is a simple INI or YAML file listing your hosts and their variables:
# /etc/ansible/hosts (INI format)
[web]
web1 ansible_host=10.0.0.11
web2 ansible_host=10.0.0.12
[db]
db1 ansible_host=10.0.0.21
[web:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/vps_ed25519
Verify connectivity across the fleet in one shot:
ansible all -m ping
ansible web -a "uptime" # run any command
ansible all -m apt -a "update_cache=true upgrade=dist" -b
The last command updates and upgrades every host in the inventory with a single invocation — the fastest way to close a security patch window across ten servers. Add --check to any playbook run to preview changes without applying them.
Your First Playbook: Base Security Hardening
A playbook is a list of plays, each targeting a group of hosts. This one installs the base packages, configures the firewall, and locks down SSH across the fleet — the same steps you would otherwise repeat manually on every box:
---
- name: Base hardening for all VPS hosts
hosts: all
become: true
vars:
ssh_port: 22
tasks:
- name: Install essential packages
apt:
name: ["ufw", "fail2ban", "ntp", "curl"]
state: present
update_cache: true
- name: Allow SSH and web traffic
ufw:
rule: allow
port: "{{ ssh_port }}"
proto: tcp
- name: Enable firewall
ufw:
state: enabled
- name: Disable root SSH login
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^PermitRootLogin"
line: "PermitRootLogin no"
notify: restart ssh
handlers:
- name: restart ssh
systemd:
name: ssh
state: restarted
Two concepts make this playbook reusable. First, notify/handlers: the SSH restart runs only when the config actually changed, so most runs are no-ops. Second, idempotency: every task describes a desired state, and re-running the playbook produces no duplicate changes. Run it with ansible-playbook hardening.yml, then ansible-playbook hardening.yml --check on a new host before adding it to the fleet. When you only need to touch a subset — say, re-run the firewall task on the web group — add tags: [firewall] to the relevant tasks and invoke with ansible-playbook hardening.yml --tags firewall --limit web. The --limit flag also lets you test a brand-new server in isolation (--limit web3) before it joins the full run.
Secrets, Variables, and Group-Specific Config
Hardcoding passwords or API keys in playbooks is how credentials leak into git history. Use ansible-vault to encrypt sensitive files:
ansible-vault create group_vars/all/vault.yml # store secrets
ansible-vault encrypt group_vars/all/vault.yml # encrypt existing
ansible-playbook hardening.yml --ask-vault-pass # prompt at runtime
Variables can be scoped per group, per host, or per file. Put generic values in group_vars/all/, web-specific ones in group_vars/web/, and per-server overrides in host_vars/web1.yml. This is what keeps one playbook correct for a fleet where every node is slightly different.
Scheduling Maintenance Runs
Security is a recurring job, not a one-time setup. Run your update and audit playbooks on a schedule with a cron entry on the control node (or a systemd timer for tighter control):
# every Sunday at 03:00, apply updates and log the result
0 3 * * 0 /usr/local/bin/ansible-playbook /opt/ansible/updates.yml \
--vault-password-file /etc/ansible/.vault_pass >> /var/log/ansible-updates.log 2>&1
Combine this with a playbook that greps for failed SSH logins and unauthorized listening ports, and you have a lightweight compliance loop: the same tool that provisions the fleet also audits it. For larger teams, AWX or Semaphore adds a web UI and job history on top of the same playbooks, but the CLI + cron approach keeps a small fleet fully automated with zero extra infrastructure.
Start with one hardening playbook, extend it to deploy application configs, and you will never manually configure a server again. If you are standing up a new fleet and want providers that behave identically across nodes, see the full specs and pricing before you commit.
Ansible needs unrestricted SSH and sudo on every target, which makes unmanaged hosting the right fit. InterServer’s VPS plans provide full root access and flat monthly pricing, so spinning up additional nodes for your fleet does not surprise your budget.




Leave a Reply
You must be logged in to post a comment.