Why is configuring Apache on five servers cleaner with Ansible than with a Bash loop over SSH?
The Bash loop only lists commands and has to hand-build error handling, connection checks and idempotency; the Ansible playbook declares the desired state per host and gets all three from Ansible and its modules.
A typical Bash attempt loops over a list of server names and runs ssh "$server" "sudo apt-get update && sudo apt-get install -y apache2 && echo 'ServerName localhost' | sudo tee -a /etc/apache2/apache2.conf && sudo systemctl restart apache2" || echo "error on $server". It works once, but:
- Idempotency is missing.
tee -aappends: each run adds anotherServerNameline to the config. And Apache is restarted every time, even when nothing changed. - Errors are coarse. One
||at the end only tells you that something failed on a host, not which step. - Connections are not managed. Unreachable hosts, host keys and parallelism are all your problem.
The Ansible version targets the group webservers, sets become: true and lists three tasks: apt with name: apache2, state: present; lineinfile with the ServerName localhost line; service with state: restarted. lineinfile makes sure the line exists exactly once, apt installs only if missing, and Ansible reports per host and per task what was ok, changed or failed. The playbook is also readable as documentation of what a web server should look like.
(Even this playbook still restarts Apache on every run; the clean fix is a handler that restarts only when the config changed.)
Go deeper:
lineinfile module (official docs) — how it guarantees a line exists once instead of appending.
Ansible 101 - Episode 1 - Introduction to Ansible (Jeff Geerling) — starts from shell scripts and shows why a declarative playbook replaces them.