ansible [pattern] -m module -a "args"
ansible-playbook [options] playbook.yml
Ansible configures servers over SSH. You list your machines in an inventory and describe
the state you want in YAML playbooks. No agent runs on the servers: Ansible connects,
does the work, and leaves. Good playbooks are idempotent: running one twice changes
nothing the second time.
The main tools
ansible Run a single module on hosts (ad-hoc commands)
ansible-playbook Run a playbook
ansible-inventory Show and check inventory
ansible-galaxy Install roles and collections
ansible-vault Encrypt secrets
ansible-doc Read module documentation offline
ansible-config View and dump configuration
ansible-lint Check playbooks for problems (separate install)
Install
sudo apt install ansible # Debian/Ubuntu
sudo dnf install ansible-core # Fedora/RHEL family
pipx install --include-deps ansible # any system, isolated from the OS Python
ansible --version # shows version, config file and module paths
The machine you run Ansible from is the "control node" (Linux/macOS/WSL).
Managed hosts need SSH and Python. Windows targets use WinRM instead.
Inventory (your list of hosts)
# inventory.ini
[web]
web1.example.com
web2.example.com ansible_port=2222
[db]
db1.example.com ansible_user=admin
[prod:children]
web
db
[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/id_ed25519
Or in YAML (inventory.yml):
all:
children:
web:
hosts:
web1.example.com:
web2.example.com:
ansible-inventory -i inventory.ini --list # see how Ansible reads it
ansible-inventory -i inventory.ini --graph # tree view
Always defined: "all" (every host) and "ungrouped".
Ad-hoc commands (one-off jobs)
ansible all -m ping # can I connect?
ansible web -m command -a "uptime" # run a command
ansible web -m shell -a "df -h | head" # shell features (pipes) need -m shell
ansible all -m setup # gather facts about the hosts
ansible all -m setup -a "filter=ansible_distribution*"
ansible web -b -m apt -a "name=nginx state=present update_cache=yes"
ansible web -b -m service -a "name=nginx state=restarted"
ansible web -m copy -a "src=app.conf dest=/etc/app.conf" -b
ansible web -m file -a "path=/tmp/x state=absent"
ansible web -m user -a "name=alice groups=sudo append=yes" -b
ansible web -m fetch -a "src=/var/log/syslog dest=./logs/"
ansible web -b -a "reboot" -f 1 # one host at a time (-f = forks)
Common options
-i FILE Inventory file (or directory)
-m MODULE Module to run (default: command)
-a "ARGS" Module arguments
-b Become root (sudo) --become-user USER become another user
-K Ask for the sudo password -k ask for the SSH password
-u USER SSH user
--private-key Key file to use
-l PATTERN Limit to a host or group (--limit)
-f N Forks: how many hosts in parallel (default 5)
-v / -vvv More detail (up to -vvvv for connection debugging)
-e "k=v" Extra variable (also -e @vars.yml)
-C Check mode: dry run, change nothing
-D Diff mode: show file changes
-t TAG Only tasks with this tag --skip-tags TAG skip them
--list-hosts Show which hosts would be used
--syntax-check Parse the playbook only
Host patterns
all Every host
web A group
web:db Web OR db
web:&prod Web AND prod
web:!staging Web but NOT staging
web* Wildcard on names
web[0:2] A slice of a group
A minimal playbook (site.yml)
---
- name: Configure web servers
hosts: web
become: true
vars:
app_port: 8080
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Deploy the config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
mode: "0644"
notify: Restart nginx
- name: Make sure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Run it
ansible-playbook -i inventory.ini site.yml
ansible-playbook site.yml -C -D # dry run with a diff (do this first)
ansible-playbook site.yml -l web1.example.com # one host only
ansible-playbook site.yml --tags config
ansible-playbook site.yml -e "app_port=9090"
ansible-playbook site.yml --start-at-task "Deploy the config"
ansible-playbook site.yml --step # confirm each task
ansible-playbook site.yml -K # ask for the sudo password
ansible-playbook site.yml --list-tasks
Handy modules
apt / dnf / package Install packages
service / systemd Start, stop, enable services
copy Copy a file or literal content to the host
template Render a Jinja2 template to the host
file Files, directories, links, permissions, delete
lineinfile / blockinfile Edit lines or blocks in a file
user / group Manage accounts
authorized_key Manage SSH keys
git Check out a repository
get_url / uri Download a file / call a web API
command / shell / script Run commands (use a real module when one exists)
cron Manage cron jobs
ufw / firewalld Firewall rules
mount, lvol, filesystem Storage
docker_container Manage containers (community.docker collection)
debug Print a message or variable
assert / fail Stop when a condition is not met
reboot Reboot and wait for the host to return
wait_for Wait for a port, file or string
Use the full name (ansible.builtin.apt). Read docs offline: ansible-doc ansible.builtin.apt
Variables
- Playbook: vars: at play or task level
- Files: vars_files: [vars/main.yml]
- Inventory: group_vars/web.yml and host_vars/web1.yml (auto-loaded folders)
- Command line: -e "key=value" (highest priority)
- Facts: ansible_hostname, ansible_distribution, ansible_default_ipv4.address
- Registered: register: result then result.stdout, result.rc, result.changed
Use them with Jinja2: "{{ app_port }}" (quote the whole string when it starts with {{).
Loops, conditions and results
- name: Install several packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: [git, curl, htop]
- name: Only on Debian family
ansible.builtin.apt:
name: apache2
when: ansible_os_family == "Debian"
- name: Check a service
ansible.builtin.command: systemctl is-active nginx
register: nginx_state
changed_when: false
failed_when: nginx_state.rc not in [0, 3]
- ansible.builtin.debug:
var: nginx_state.stdout
Handling errors and control
ignore_errors: true # keep going if this task fails
block / rescue / always # try/catch/finally for tasks
serial: 2 # roll out two hosts at a time
max_fail_percentage: 20 # abort if too many hosts fail
run_once: true # do it on one host only
delegate_to: localhost # run the task somewhere else
tags: [config, nginx] # select with --tags
no_log: true # hide secrets from output
check_mode: false # task runs even under -C
throttle: 1 # limit parallelism for one task
Templates (Jinja2)
# templates/nginx.conf.j2
server {
listen {{ app_port }};
server_name {{ inventory_hostname }};
{% for site in sites %}
include /etc/nginx/sites/{{ site }}.conf;
{% endfor %}
}
Filters: {{ name | upper }} {{ list | join(",") }} {{ var | default("x") }} {{ path | basename }}
{{ items | length }} {{ value | int }} {{ data | to_json }} {{ pass | password_hash("sha512") }}
Roles (reusable structure)
ansible-galaxy init roles/webserver
roles/webserver/
tasks/main.yml Tasks
handlers/main.yml Handlers
templates/ Jinja2 files
files/ Static files
defaults/main.yml Default variables (lowest priority)
vars/main.yml Role variables (higher priority)
meta/main.yml Dependencies
Use in a play: roles: [webserver] or roles: [{ role: webserver, app_port: 9090 }]
Collections and Galaxy
ansible-galaxy collection install community.general
ansible-galaxy collection install -r requirements.yml
ansible-galaxy role install geerlingguy.nginx
ansible-galaxy collection list
Secrets with Vault
ansible-vault create secrets.yml # new encrypted file
ansible-vault edit secrets.yml
ansible-vault encrypt vars/db.yml
ansible-vault decrypt vars/db.yml
ansible-vault view secrets.yml
ansible-vault encrypt_string 'S3cret!' --name 'db_password' # encrypt one value
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Put the vault password file outside the repo and out of git.
ansible.cfg (project settings)
[defaults]
inventory = ./inventory.ini
remote_user = ubuntu
host_key_checking = True
forks = 10
stdout_callback = yaml
retry_files_enabled = False
[privilege_escalation]
become = True
Ansible reads ./ansible.cfg first, then ~/.ansible.cfg, then /etc/ansible/ansible.cfg.
ansible-config dump --only-changed # show what differs from the defaults
Practical one-liners
# Update every server (apt) and reboot if required
ansible all -b -m apt -a "upgrade=dist update_cache=yes"
ansible all -b -m reboot
# Free disk space on every host
ansible all -m shell -a "df -h /"
# Who is logged in everywhere?
ansible all -m command -a "who"
# Copy your SSH key to all hosts
ansible all -m authorized_key -a "user=ubuntu key='{{ lookup('file','~/.ssh/id_ed25519.pub') }}'"
# Restart a service on one group, one host at a time
ansible web -b -m service -a "name=nginx state=restarted" -f 1
# Find which hosts a playbook would touch
ansible-playbook site.yml --list-hosts
# Lint before you commit
ansible-lint site.yml
# Run a role against a single host, quickly
ansible-playbook -i "web1.example.com," -u ubuntu -b -e "target=web1" site.yml
Common errors
UNREACHABLE! Permission denied (publickey) Wrong user or key: check -u and --private-key
Missing sudo password Add -K or set up passwordless sudo
"msg": "Failed to connect to the host" Firewall, wrong port or host is down
Host key verification failed Accept the key with ssh first, or manage known_hosts
Python interpreter not found Install python3 on the target or set ansible_python_interpreter
Syntax error in YAML Check indentation (spaces, never tabs) and quote {{ }} values
'variable' is undefined Typo or missing var: check group_vars and -e
Module not found Install the collection with ansible-galaxy
Tips
Run with -C -D (check and diff) before the real run, on staging before production.
Prefer a proper module over command or shell. Modules are idempotent and report changes correctly.
YAML is strict about indentation: two spaces, no tabs. Quote any value starting with {{.
Keep playbooks and roles in git, and keep secrets in Ansible Vault (or a secrets manager), never in plain text.
Use groups and group_vars, not lots of per-host settings, so adding a server is one line.
Give every task a clear name so the output reads like a report.
Use handlers for restarts so a service restarts once, and only when something actually changed.
Set serial when rolling changes across a fleet, so one bad task cannot take everything down at once.
Use SSH keys with an agent, not passwords, and limit who can use become.
Pin collections in requirements.yml so a playbook behaves the same next month.