Independent comparison · no paid rankings
Home / Blog / Ansible: configure servers without creating a collection of exceptions
Technical

Ansible: configure servers without creating a collection of exceptions

Ansible promises idempotence — until each host accumulates host_vars, "just for prod-03" tasks and an inventory nobody dares refactor.

4 min read Updated Jul 19, 2026

First Ansible deploy was elegant: webstack role, three groups staging, prod, bastion. A year later inventory has prod-legacy, twelve "temporary" host_vars/prod-03.yml blocks, and when: inventory_hostname == 'prod-03' in nginx role. Playbook passes — but nobody knows which config is norm and which is patch.

Ansible scales poorly when every exception stays local instead of rising to variable or documented optional role. Classic trap: playbook passes in CI but documents twelve host_vars patches. New engineer no longer knows which config is standard. Goal is not green run — it is that second ansible-playbook changes nothing if machine has not drifted. Without this discipline, you automate confusion rather than desired config.

Inventory, group_vars and clear hierarchy

Recommended structure: inventories per environment, group_vars/all.yml, group_vars/web.yml, decoupled roles. group_vars for everything a group shares. host_vars for real exceptions (dedicated IP, client cert) — with comment and ticket. No when on hostname in roles — prefer nginx_extra_vhosts variable.

Every when: inventory_hostname == is debt shouting.

Roles, tags and change surface

Split by responsibility: common, web, db_client. Tags limit blast radius in production. Document defaults in roles/x/defaults/main.yml — overrides in group_vars only. Role mixing nginx, PHP and firewall becomes impossible to test in isolation.

Vault, SSH and bastion access

Encrypt secrets with ansible-vault encrypt. SSH keys via agent — no shared root key. Single bastion, ProxyJump, limited sudo. Align with what your VPS host allows — some block direct root access, others require key per environment.

Broken idempotence: symptoms

shell: curl ... | bash every run. copy without checksum. Systematic service: state=restarted. Fix with declarative modules. Manual drift → import to code or block direct SSH. Hand-modified machine contradicts automation — and nobody knows which source is truth.

CI, Molecule and staging parity

Pipeline: ansible-lint, yamllint, Molecule converge twice — second run must report 0 changed. Staging must mirror prod (same roles, different vars). Without parity, you test config that does not exist in production. Version inventory in same repo as roles — unversioned local inventory is most frequent source of "worked on my machine" in prod.

Refactor without paralysing the team

Count host_vars and hostname when: — if rising, plan refactor pause. Raise exceptions into documented group variables, one per sprint. Ansible quickly executes collection of exceptions — it does not fix them.

On VPS or bare metal, config also drifts via manual changes: forgotten SSH fix, hand-installed package. Blocking direct production access — except documented emergency — forces return to code. Cross-check TLS and hardening: same declarative configuration principles apply beyond Ansible alone.

The climax: automation hiding inconsistency

Decide and move forward without blind spots

Refactor exceptions into documented group variables, one by one, with tracking ticket. Add Molecule in CI and require zero change on second run. Encrypt all secrets with ansible-vault and revoke any secret already committed in clear. Document bastion and SSH keys per environment — who has access, which key, which rotation. Count host_vars and hostname when:: if rising, pause refactor. Compare VPS suited to Ansible via directory and compare tool.

Frequently asked questions

Roles or monolithic playbook?

Reusable roles plus group_vars; avoid catch-all playbook. Monolithic playbook becomes untested exception collection once it exceeds few roles.

How to test before prod?

Molecule with second run at zero change, parity staging, CI lint. --check alone is not enough — it ignores some side effects.

Does Ansible handle secrets well?

ansible-vault mandatory; no clear secrets in git. Revoke any secret already exposed in history — later encryption does not erase the past.

Why not idempotent?

Non-declarative shell/command, forced restarts, per-host exceptions. Each when: inventory_hostname == in a role is alarm signal.


Healthy inventory reads in groups — not server anecdotes.

Compare European hosts

Filter by compliance, location and use case — then open the sheets to verify the real scope.

Browse the directory
Blog

Related reading

All articles →