Why Ansible for a VPS Fleet?
When managing multiple servers, the natural temptation is to write Bash scripts. They're quick to get started, but scripts are not idempotent: run them twice and you risk duplicating entries, overwriting files, or breaking a working configuration. Fabric improves the Python ergonomics but stays in the same imperative mindset. Puppet and Chef are powerful tools, but they require an agent on every node, a master server to maintain, and a steep learning curve for a team that simply wants to keep their VPS instances consistent. Ansible takes a different approach. It works in push mode, over SSH, without installing anything on the target hosts. Each playbook describes a final state rather than a sequence of actions. If you run it a second time on an already-configured server, Ansible changes nothing: this is the idempotence property, and it is fundamental to operating a fleet with confidence. For a web agency delivering projects on client VPS instances, or for a DevOps team standardizing staging and production environments, Ansible offers an excellent effort-to-benefit ratio on the market: accessible YAML syntax, a massive community, and natural integration into existing CI/CD pipelines.
8 Reasons to Choose Ansible for Your VPS Servers
- Agentless over SSH: no daemon to install on your hosts. Ansible connects via SSH using your existing keys, reducing the attack surface and eliminating agent maintenance overhead.
- Native idempotence: every Ansible module guarantees that re-running a playbook on an already-configured system produces no spurious changes. You can safely run your playbooks against a production fleet.
- Readable YAML syntax: playbooks read like documentation. A developer unfamiliar with Ansible can understand what a role does in minutes, making code review and onboarding easier.
- Dynamic inventory: beyond static
hosts.inifiles, Ansible can query your cloud provider or an internal API to build the inventory on the fly. Ideal when VPS instances are created and destroyed frequently. - Reusable roles: the
roles/structure lets you package a configuration (nginx, PostgreSQL, SSH hardening) and reuse it across projects without copy-pasting playbooks. - Ansible Vault for secrets: passwords, API keys and certificates are encrypted directly in the Git repository with
ansible-vault. No more plaintext secrets in scripts or unversioned environment variables. - Ansible Galaxy: a community ecosystem of roles (geerlingguy, devsec) covers the most common use cases. Rather than writing an SSH hardening role from scratch, you import one audited by thousands of users.
- CI/CD compatible: a playbook runs from a GitHub Actions or GitLab CI pipeline with exactly the same command as locally. Every merge to
maincan automatically trigger configuration deployment across your fleet.
Prerequisites: What You Need Before Starting
Before writing your first playbook, a few prerequisites need to be checked on the control machine and on the target servers.
On the control machine (your local workstation or a dedicated orchestration VPS), you need Python 3.8 or higher and Ansible 2.14 minimum. Ansible does not run natively on Windows as a control machine: if you are on Windows, use WSL2 or a Docker container.
On the target VPS servers, requirements are minimal: SSH access with a user that has sudo privileges, Python 3 installed (present by default on Debian 11+, Ubuntu 20.04+ and Rocky Linux 8+), and your public SSH key already deployed on each host. If you are starting from freshly provisioned servers, initial root access is sufficient for the first configuration pass.
Organize your workspace in a Git repository from the start. Versioning your inventory and playbooks is the only way to know which configuration was applied to which machine, and when. An ansible.cfg file at the project root centralizes settings (inventory path, remote user, SSH key) to avoid repeating them on the command line.
From Installation to Your First Playbook in 7 Steps
Install Ansible on the control machine
The recommended method is pip install ansible inside a Python virtual environment, giving you the latest stable version regardless of your distribution. On Debian/Ubuntu, apt install ansible also works but often installs an older version. Verify with ansible --version that the installation is correct and note the configuration path.
Create the inventory with your server groups
Create an inventory/hosts.ini file and organize your VPS instances into logical groups: [web] for application servers, [db] for databases, [mail] for mail servers. Each host is listed by its IP or DNS name, optionally with ansible_user=ubuntu if the SSH user differs. Groups make it easy to apply targeted roles.
Test connectivity with the ping module
Before any playbook, validate that the inventory is correct and that SSH works: ansible -i inventory/hosts.ini all -m ping. Each host should respond with pong. If a machine fails, check the username, SSH key and that Python 3 is available on the target. This basic test prevents you from debugging a playbook when the problem is actually in the transport layer.
Write an initial hardening playbook
Create playbooks/hardening.yml with three essential tasks: create a non-root admin user with their public key, modify sshd_config to disable password authentication and direct root login, then configure ufw with a default deny policy and only the allowed ports (22, 80, 443). Use Ansible's user, lineinfile and ufw modules.
Run in dry-run mode with --check
Before applying the playbook to your servers, run ansible-playbook --check playbooks/hardening.yml. The --check flag simulates execution without modifying anything: Ansible tells you exactly which tasks would have produced a change (changed) and which would not (ok). Fix any syntax or logic errors before the real application.
Apply and verify idempotence
Run the playbook without --check for the real application. Note the number of changed tasks. Immediately run it a second time: if your playbook is correctly written, the changed counter should be zero. This idempotence verification is the fundamental quality criterion for an Ansible playbook. A playbook that produces changes on the second run hides a logic bug.
Refactor into a reusable role
Once the playbook is stable, convert it to a role with ansible-galaxy init roles/hardening. Move the tasks to roles/hardening/tasks/main.yml, default variables to defaults/main.yml, and handlers (like reloading sshd) to handlers/main.yml. The role becomes a reusable block you can apply to any host group from any project.
Managing Secrets with Ansible Vault
In a server fleet, secrets are everywhere: database passwords, third-party API keys, TLS certificates, Docker registry access tokens. The temptation to store them in plaintext in variable files is strong, especially when working alone. This is a major risk as soon as the repository becomes shared or a developer leaves the team.
Ansible Vault encrypts your variable files directly in the Git repository. The command ansible-vault create vars/secrets.yml opens an editor and encrypts the result with a master password (or a vault key). The encrypted file can be safely versioned: without the password, its contents are unreadable.
For a team, it is recommended to use a password file (--vault-password-file ~/.vault_pass) rather than typing the password at each execution. This file is stored outside the repository and distributed via a secrets manager like HashiCorp Vault or your CI/CD pipeline. GitHub Actions and GitLab CI allow storing the Vault password as an environment secret, making pipeline playbook execution as simple as running locally.
A good practice: separate your variables into two files — vars/main.yml for non-sensitive values (domain names, ports, versions) and vars/secrets.yml encrypted for secrets. Your playbooks read both, and only secrets.yml requires the Vault.
If some of your VPS instances are behind strict NAT or a firewall that blocks inbound SSH connections from your control machine, Ansible's push mode does not work. The solution is ansible-pull: install Ansible on each target host, then configure a cron job or systemd timer that runs ansible-pull -U <repo-url> at regular intervals. Each server pulls its configuration from Git and applies it locally. This is the reverse of the usual mode, but idempotence and roles work exactly the same way. Also useful for air-gapped environments where only the server has access to the internal repository.
Going Further: Dynamic Inventory and AWX
A static hosts.ini inventory works perfectly for a stable fleet. But if you provision and destroy VPS instances frequently — for ephemeral staging environments or fixed-term client projects — maintaining the file by hand becomes a source of errors.
Dynamic inventory solves this problem: Ansible can query an API (your cloud provider, Netbox, a custom script) to build the host list on the fly before each execution. The community.general.cobbler plugin or a Python script returning structured JSON is sufficient in most cases.
For teams that want a graphical interface and role-based access control, AWX (the open-source version of Red Hat Ansible Automation Platform) or Semaphore (lighter, suited for small teams) provide a web UI for managing inventories, triggering playbooks, scheduling runs, and auditing past executions. AWX installs on a dedicated VPS and exposes a REST API: you can trigger a playbook from a CI pipeline, from a webhook, or from a button in your internal tooling.
Adopting these tools marks the transition from personal Ansible administration to a structured team practice, where every execution is traced, approved, and associated with an identified user.
Conclusion: A Declarative and Reproducible Infrastructure
Ansible is not a magic tool, but it precisely addresses the daily problem of any team managing multiple VPS instances: how do you ensure that the actual state of each server matches what was intended, without spending hours manually comparing configurations? By adopting a declarative approach — describing what you want rather than how to get it — you gain reproducibility, traceability, and confidence. A new team member can understand your infrastructure state by reading the playbooks. A server that goes down can be rebuilt from scratch in minutes using the same inventory. A security audit becomes a YAML reading exercise rather than a machine-by-machine inspection. Start small: an initial hardening playbook applied to your existing VPS instances. Version it, test idempotence, then add roles as needs arise. The initial investment is modest, and the gains in time and reliability become apparent from the second or third managed machine.