Ansible for Beginners: Automate Server Configuration
Ansible for Beginners: Automate Server Configuration
Ansible is configuration management and orchestration over SSH — no agents, no PKI infrastructure, no central daemon. You write YAML describing the state you want, run it from your laptop or CI, and Ansible pushes that state to every node you point it at. Where Chef and Puppet pull configuration on a schedule, Ansible pushes on demand — which means nodes only need to be reachable, not registered. This guide follows the official Ansible documentation's path — concepts → inventory → ad-hoc commands → playbooks → roles — using the same vocabulary you'll find in the docs.
Table of Contents
- The three components
- Step 0 — Install and set up a lab
- Step 1 — The inventory
- Step 2 — Ad-hoc commands and facts
- Step 3 — Groups and variables
- Step 4 — Your first playbook
- Step 5 — Pushing files
- Step 6 — Handling failures
- Step 7 — Conditionals
- Step 8 — Deploying code with the git module
- Step 9 — Scaling to multiple hosts
- Step 10 — Templates (Jinja2)
- Step 11 — Variable precedence (the part that bites)
- Step 12 — Roles: structure that scales
- Step 13 — Tags: run slices of a playbook
- Configuring Ansible: ansible.cfg
- Collections and Galaxy
- Commands you'll use daily
The three components
The official docs describe every Ansible environment as three parts — learn these names now because the documentation uses them everywhere:
- Control node — the machine Ansible is installed on, where you run
ansibleandansible-playbook. Your laptop is a control node. Multiple control nodes are fine, but Ansible doesn't coordinate between them. - Managed nodes — the remote machines Ansible controls, also called "hosts." Ansible is not installed on managed nodes; they just need SSH (Linux) or WinRM (Windows) plus Python.
- Inventory — the list of managed nodes, logically organized into groups, living on the control node.
The other official vocabulary you'll meet constantly: a playbook is a YAML list of plays; a play maps managed nodes to an ordered list of tasks; each task invokes a module (the unit of code Ansible copies to and executes on the node); a handler is a task that runs only when notified by a change. Modules, roles, and plugins are distributed in collections via Ansible Galaxy. Every module has a Fully Qualified Collection Name (FQCN) — ping is really ansible.builtin.ping — and docs reference modules by FQCN.
Step 0 — Install and set up a lab
Two packages exist: ansible (the community package — batteries included, ~100 collections) and ansible-core (the engine plus ansible.builtin only — what most teams actually pin). The docs recommend pipx, which installs each package in an isolated environment automatically:
pipx install ansible # community package
# or: pipx install ansible-core
# classic alternative:
python3 -m venv ~/venvs/ansible && source ~/venvs/ansible/bin/activate
pip install ansible
ansible --version confirms it. You also need target machines — easiest lab: Vagrant or a couple of Docker containers with SSH. Assume two test hosts — 192.168.60.10 and 192.168.60.11 — reachable with your SSH key.
Step 1 — The inventory
The inventory tells Ansible what exists. INI format, one host per line:
# hosts.ini
192.168.60.10
192.168.60.11
Test connectivity with the ping module (Ansible's ping tests Python-over-SSH, not ICMP):
ansible -i hosts.ini all -m ping -u vagrant
all is a built-in group containing every host. A pong reply means Ansible can SSH in and execute modules.
Step 2 — Ad-hoc commands and facts
Before playbooks, Ansible runs modules directly — this is how you debug and explore:
ansible -i hosts.ini all -m command -a "uptime"
ansible -i hosts.ini all -m apt -a "name=htop state=present" --become
Every task is a module call: command, apt, copy, service, user. Modules are idempotent — state=present installs htop once and reports ok (no change) on re-runs, not changed.
Gather facts — Ansible's auto-detected info about each node (OS, IPs, memory, disks):
ansible -i hosts.ini all -m setup | less
ansible -i hosts.ini all -m setup -a "filter=ansible_distribution*"
Step 3 — Groups and variables
Real inventories group hosts and attach variables:
[web]
192.168.60.10
[db]
192.168.60.11
[prod:children]
web
db
[all:vars]
ansible_user=vagrant
app_port=8080
Now -m ping can target web, db, or prod. Variables defined here are usable in playbooks as {{ app_port }}.
Step 4 — Your first playbook
A playbook is a YAML file of plays; a play maps tasks to hosts:
# playbook.yml
- name: Configure web servers
hosts: web
become: true
tasks:
- name: Install nginx
apt:
name: nginx
state: present
update_cache: true
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: true
ansible-playbook -i hosts.ini playbook.yml
Run it twice — the second run should show all ok, zero changed. That idempotency is the whole point: playbooks are safe to re-run continuously.
Step 5 — Pushing files
- name: Deploy a static page
copy:
src: files/index.html
dest: /var/www/html/index.html
mode: "0644"
src resolves relative to the playbook directory — keep files in files/ next to it.
Step 6 — Handling failures
A failed task stops that host's play. Control it:
- name: Optional health check
command: /usr/local/bin/healthcheck
ignore_errors: true
- name: Restart only when the config changed
service:
name: nginx
state: restarted
# ...triggered by a handler, below
Pair ignore_errors with register + failed_when when you need finer control:
- name: Check cluster membership
command: cluster-tool status
register: result
failed_when: "'DEGRADED' in result.stdout"
Step 7 — Conditionals
- name: Install apache2 on Debian family
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Open firewall only for prod
ufw:
rule: allow
port: "{{ app_port }}"
when: env == "production"
Step 8 — Deploying code with the git module
- name: Deploy app from git
git:
repo: "https://github.com/yourorg/app.git"
dest: /opt/app
version: v1.4.2
notify: restart app
notify triggers a handler — a task that runs once at the end, only if something changed:
handlers:
- name: restart app
service:
name: app
state: restarted
This is the standard pattern: change file → notify → service restarts once, even if five tasks notified.
Step 9 — Scaling to multiple hosts
Change hosts: web to hosts: prod and the same play runs across the group in parallel. Control it:
serial: 2— rolling execution, 2 hosts at a time (how you do zero-downtime deploys)-l webon the CLI — limit to a subset--check— dry-run mode; add--diffto preview file changes
Step 10 — Templates (Jinja2)
copy pushes static files; template renders Jinja2 on the target:
- name: Render nginx vhost
template:
src: templates/vhost.conf.j2
dest: /etc/nginx/sites-available/app.conf
notify: reload nginx
# templates/vhost.conf.j2
server {
listen 80;
server_name {{ domain }};
location / {
proxy_pass http://127.0.0.1:{{ app_port }};
}
}
{{ domain }} comes from inventory vars, group_vars/, host_vars/, or -e domain=example.com at the CLI.
Step 11 — Variable precedence (the part that bites)
Ansible has ~20 places to define variables. Remember this simplified order, weakest to strongest:
- defaults/main.yml inside a role — lowest priority, always overridable
- Inventory group_vars / host_vars
- Playbook vars: block
- --extra-vars on the CLI — almost always wins
Rule of thumb: defaults in roles, environment differences in group_vars, one-off overrides with -e. When a variable "mysteriously" won't change, something stronger is beating you.
Step 12 — Roles: structure that scales
When a playbook grows past ~50 lines, split it into roles — the standard directory layout:
roles/
└── nginx/
├── tasks/main.yml # the tasks
├── handlers/main.yml # restart/reload handlers
├── templates/ # .j2 files (src resolves here automatically)
├── files/ # static files
├── defaults/main.yml # lowest-precedence variables
└── meta/main.yml # role dependencies
The playbook shrinks to intent:
- name: Production web tier
hosts: web
become: true
roles:
- common
- nginx
- app
Step 13 — Tags: run slices of a playbook
- name: Render nginx vhost
template:
src: templates/vhost.conf.j2
dest: /etc/nginx/sites-available/app.conf
tags: [nginx, config]
ansible-playbook -i hosts.ini site.yml --tags config # only tagged tasks
ansible-playbook -i hosts.ini site.yml --skip-tags slow # skip a slice
Configuring Ansible: ansible.cfg
Stop passing -i hosts.ini on every command — put defaults in ansible.cfg at the project root:
[defaults]
inventory = hosts.ini
host_key_checking = False # lab only — real setups verify host keys
retry_files_enabled = False
forks = 20 # parallel hosts per play
Config resolution order (later wins): ANSIBLE_CONFIG env var → ./ansible.cfg → ~/.ansible.cfg → /etc/ansible/ansible.cfg. A project-local ansible.cfg is the pattern — it travels with the repo, so every teammate gets identical behavior.
Collections and Galaxy
Almost everything Ansible ships — modules, roles, plugins — is packaged in collections and distributed through Ansible Galaxy. You already used FQCN names implicitly; reference them fully when ambiguity matters:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
Install non-builtin collections with ansible-galaxy:
ansible-galaxy collection install community.docker
ansible-galaxy collection install amazon.aws
Pin them in requirements.yml so teammates get identical collections:
collections:
- name: community.docker
version: ">=3.4,<4.0"
ansible-galaxy collection install -r requirements.yml
Commands you'll use daily
ansible-playbook site.yml --check --diff # dry run with file diffs
ansible-playbook site.yml -l web # limit to a group
ansible-inventory --list -i hosts.ini # dump resolved inventory/vars
ansible-vault encrypt secrets.yml # encrypt variable files
ansible-doc copy # module docs offline
Where to go next: the official Ansible documentation (source lives in ansible/ansible-documentation) is organized as getting started → how-to guides → module/plugin indexes — ansible-doc <module> gives you the same reference offline. Then look at ansible-vault for secrets (and compare with Vault for dynamic credentials), the community.* collections on Galaxy, and AWX when the team needs scheduled runs, RBAC, and a UI.
Related Articles
- Essential Linux Commands Every DevOps Engineer Needs
- AWS for DevOps Engineers: The Services That Actually Matter
Last Updated: October 2026 Author: CloudOpsGuide Team Difficulty: Beginner Estimated Reading Time: 18 minutes