How to Manage Several Servers with Ansible (Run the Same Change on All of Them)

Once there's a server, a Pi, a VPS and a backup box at a relative's, "apply updates", "add my new SSH key" and "set the time zone" become four SSH sessions each. Ansible does them in one: it connects over SSH to every machine in a list and runs tasks described in YAML — install this package, ensure this line is in that file, start this service — and only changes what needs changing. Nothing to install on the targets but Python, which Ubuntu already has.
Quick reference
| Task | Command |
|---|---|
| Install (on the control machine) | pipx install ansible or sudo apt install ansible -y |
| Inventory | inventory.ini listing hosts and groups |
| Can I reach them? | ansible all -m ping |
| Run a command everywhere | ansible all -a "df -h /" |
| Update everything | ansible all -b -m apt -a "update_cache=yes upgrade=dist" |
| Run a playbook | ansible-playbook site.yml |
| Dry run | ansible-playbook site.yml --check --diff |
1. Install Ansible on the control machine
Your laptop (Mac/Linux/WSL) or the main server — wherever you already SSH from:
sudo apt install pipx -y && pipx install ansible
ansible --version
2. The inventory
mkdir -p ~/ansible && cd ~/ansible
nano inventory.ini
[home]
homeserver ansible_host=192.168.1.50
pi ansible_host=192.168.1.60
[remote]
vps ansible_host=203.0.113.10
mum ansible_host=192.168.2.50 # over the site-to-site tunnel
[all:vars]
ansible_user=john
ansible_python_interpreter=/usr/bin/python3
SSH keys must already work to each host as john (tutorial 3). Test:
ansible -i inventory.ini all -m ping
Every host should answer pong. Save typing with ansible.cfg in the folder:
[defaults]
inventory = inventory.ini
host_key_checking = True
3. Ad-hoc commands
ansible all -a "uptime"
ansible home -a "df -h /"
ansible all -b -m apt -a "update_cache=yes upgrade=dist"
ansible all -b -m reboot --limit pi
-b means become (sudo). -m picks a module: apt, copy, service, user, shell. The apt one above is "update every machine" in one line; --limit narrows to a host or group.
4. A first playbook
A playbook is a list of tasks, each describing a state. Run it twice and the second run changes nothing — that's idempotence, and it's why Ansible beats a shell script.
nano baseline.yml
---
- name: Baseline every machine
hosts: all
become: true
vars:
my_key: "ssh-ed25519 AAAA... john-laptop"
tasks:
- name: Time zone
community.general.timezone:
name: Europe/London
- name: Packages I always want
apt:
name: [htop, tmux, ncdu, fail2ban, unattended-upgrades, ufw]
state: present
update_cache: true
- name: My SSH key on every box
authorized_key:
user: john
key: "{{ my_key }}"
- name: Password login off
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
notify: restart ssh
- name: UFW allows SSH and is on
community.general.ufw:
rule: allow
name: OpenSSH
- community.general.ufw:
state: enabled
policy: deny
- name: Fail2ban running
service:
name: fail2ban
state: started
enabled: true
handlers:
- name: restart ssh
service:
name: ssh
state: restarted
ansible-galaxy collection install community.general
ansible-playbook baseline.yml --check --diff # what would change
ansible-playbook baseline.yml
The summary shows changed= per host. Run it again: changed=0. Add a new machine to the inventory, run it once, and it's set up like the others. That's tutorials 3 to 7 as one command.
5. Useful modules
copy and template (files with variables), lineinfile / blockinfile (edit configs), user, cron, systemd, docker_compose_v2 (bring a compose stack up), git, uri (hit an API), mount, apt_repository. ansible-doc -l lists thousands; ansible-doc apt documents one.
6. Secrets
Passwords and tokens go in Ansible Vault: ansible-vault create secrets.yml, refer to the variables in playbooks, run with --ask-vault-pass. Keep the whole ~/ansible folder in Gitea (tutorial 45) — vault files are encrypted, so they're safe to commit.
Ansible and Docker
Use Ansible for the host: packages, users, firewall, Docker itself, and docker_compose_v2 to deploy stacks from your git repo. Keep the apps' own config in compose files as before.
Troubleshooting
"Permission denied (publickey)". Your key isn't on that host for ansible_user; fix with SSH first.
"Missing sudo password". Passwordless sudo isn't set on the target; add -K to be asked, or configure NOPASSWD for your user in sudoers.
Module not found (community.general.ufw). ansible-galaxy collection install community.general.
A task changes every run. It's a shell or command task — those always "change". Use the specific module, or add changed_when.
Wrapping up
An inventory, ansible all -m ping, one playbook that captures your baseline. Updates become one line, a new machine becomes one run, and the state of every box in your care is a file in git rather than a memory of what you typed where.
