Ansible for beginners
No automation experience needed. Learn to manage Linux servers with plain YAML files, and practise on three real servers running on your own Windows laptop. Work through it at your own pace in about two hours, or use it to teach a class.
TASK[Set up your lab]
You don't need Linux experience or a cloud account. Everything runs on your laptop. Ansible lives inside WSL2, the Linux environment built into Windows, and it manages three small Ubuntu servers running in Docker Desktop.
$ cd ~/ansible-lab
$ docker compose up -d
$ ansible all -m ping -k # SSH password: ansibleThree green SUCCESS lines mean you are ready. Haven't set up yet, or not working? Jump to the setup appendix, or use the backup container: docker compose exec control bash.
/mnt/c so ansible.cfg is ignored, or sshpass missing.TASK[What is Ansible?]
One server is easy. A hundred is not.
By hand
$ ssh server-01
$ sudo apt install nginx
$ sudo vi /etc/nginx/nginx.conf
$ sudo systemctl restart nginx
# ...and again for server-02 to server-100Slow, easy to get wrong, and every server drifts a little. Nobody has a record of what was changed.
With Ansible
- name: Web servers
hosts: webservers # 1 or 1000 hosts
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginxDescribe the result once and apply it everywhere. The file lives in Git, so changes are reviewed and repeatable.
Definition
Ansible is an open-source, agentless automation tool. It configures systems, deploys software and orchestrates IT work using readable YAML files, connecting over SSH to Linux and over WinRM or SSH to Windows.
- Agentless: nothing to install on the servers you manage, only SSH and Python.
- YAML: playbooks read like a checklist.
- History: created by Michael DeHaan in 2012; Red Hat bought it in 2015.
- Names you will hear:
ansible-coreis the engine with the builtin modules. Theansiblepackage adds curated community collections (that is what we installed). Red Hat Ansible Automation Platform, and its open-source upstream AWX, add a web UI for teams.
Four ideas behind Ansible
- Infrastructure as Code
- Servers are described in text files kept in Git, so every change is reviewed, versioned and repeatable.
- Configuration management
- Keep many machines in a known, consistent state: packages, files, users and services.
- Idempotency
- Running the same playbook twice gives the same result. If nothing has drifted, the second run changes nothing.
- Declarative
- You say what you want (βnginx is present and runningβ). The module works out how to get there.
What people automate with it
Configuration management, application deployment and rolling updates, provisioning cloud resources on AWS, Azure, GCP or VMware, orchestrating multi-tier changes in order (database, then app, then load balancer), security patching and compliance, and network devices from Cisco, Juniper and Arista.
TASK[Ansible vs Chef vs Puppet vs SaltStack]
| Ansible | Chef | Puppet | SaltStack | |
|---|---|---|---|---|
| Architecture | Agentless, push | Agent, pull | Agent, pull | Agent or SSH, push and pull |
| Language | YAML | Ruby DSL | Puppet DSL | YAML with Jinja |
| Transport | SSH / WinRM | HTTPS to Chef Server | HTTPS to Puppet Server | ZeroMQ event bus |
| Setup effort | Very low | High | High | Medium |
| Learning curve | Easy | Steep | Moderate to steep | Moderate |
| Execution order | Top to bottom | Top to bottom | Model-based, by dependencies | Top to bottom |
| Speed at scale | Good (parallel forks) | Good | Good | Very fast (event-driven) |
| Owner | Red Hat (IBM) | Progress | Perforce | Broadcom (VMware) |
Push vs pull
No tool is simply βbestβ. Chef and Puppet are mature and strong at continuous drift correction; Salt is very fast at large scale. Ansible wins on simplicity because there are no agents or central server to run. It can also work in pull style with ansible-pull or scheduled runs in AWX.
TASK[Why Ansible?]
- Simple
- YAML reads like a checklist. Most people are productive on day one.
- Agentless
- Only SSH and Python on targets. Nothing extra to patch, monitor or secure.
- Idempotent
- Safe to re-run; only what is different gets changed.
- Batteries included
- Thousands of modules and collections on Ansible Galaxy.
- Works everywhere
- Linux, Windows, cloud, containers and network gear.
- Big community
- A common DevOps skill in job listings, backed by Red Hat.
Honest limits: SSH-based runs across thousands of hosts are slower than agent or event-bus tools, and there is no built-in continuous drift correction unless you schedule runs.
TASK[Architecture]
The control node reads the inventory (which hosts), the playbook (what to do), and uses modules (small programs that do the work) and plugins (connection, output, lookups, filters). Nothing Ansible-specific runs on the managed nodes: no agent, no database, no daemon.
What happens when a task runs
- ReadParse
ansible.cfg, the inventory, the playbook and variables. - ConnectOpen SSH to each host, several at a time (
forks, default 5; the lab uses 10). - CopySend the module, a small Python program, to a temporary folder on the host.
- ExecuteRun it. The module compares the current state with the desired state.
- ReturnThe module replies with JSON: ok, changed or failed.
- Clean upRemove the temporary files and move on to the next task.
$ ansible-inventory --graph
$ ansible-config dump --only-changed
$ ansible web1 -m ping -vvv # watch connect, copy, execute, returnpipelining = True in the lab config skips the copy step, which speeds things up.TASK[Terminologies]
The building blocks
- Control node
- The machine Ansible is installed on and run from. Linux or macOS; on Windows, use WSL.
- Managed node (host)
- Any machine Ansible configures. Needs SSH and Python (Linux) or WinRM (Windows).
- Inventory
- The list of managed nodes, organised into groups. INI or YAML, static or dynamic.
- Module
- A unit of work Ansible sends to a host, such as
apt,copy,serviceoruser. - Task
- One call to a module with arguments, plus a descriptive name.
- Play
- Maps a group of hosts to a list of tasks.
- Playbook
- A YAML file containing one or more plays.
The power features
- Variables
- Values that differ per host, group or environment, such as
http_port: 80. - Facts
- Information Ansible discovers about each host (OS, IP, memory) using the
setupmodule. - Handlers
- Tasks that run only when another task notifies them of a change, like restarting nginx.
- Templates
- Jinja2 files (
.j2) filled in with variables to produce real config files. - Roles
- A standard folder layout that packages tasks, handlers, templates and variables for reuse.
- Collections
- The distribution format for modules, roles and plugins, such as
ansible.posix. - Ansible Galaxy
- The public hub for sharing roles and collections.
- Vault
- Built-in encryption for passwords, keys and other secrets.
Inventory: telling Ansible which hosts exist
[webservers]
web1 ansible_host=127.0.0.1 ansible_port=2221
web2 ansible_host=127.0.0.1 ansible_port=2222
[dbservers]
db1 ansible_host=127.0.0.1 ansible_port=2223
[lab:children] # a group of groups
webservers
dbserversall:
children:
lab:
children:
webservers:
hosts:
web1:
web2:
dbservers:
hosts:
db1:Every inventory has two built-in groups, all and ungrouped. You target hosts with patterns such as web1, webservers, webservers:dbservers or all:!db1. Dynamic inventory plugins can read hosts straight from AWS, Azure or VMware.
Where variables live
- -e "x=1" extra varsalways wins
- task and block vars
- play vars, vars_files
- host_vars/db1.ymlone host
- group_vars/webservers.ymlone group
- group_vars/all.ymlevery host
- role defaults/main.ymleasiest to override
# inventory/group_vars/webservers.yml
http_port: 80
page_title: "Hello from Ansible"
# inventory/host_vars/db1.yml
server_role: database
# use it anywhere with Jinja2
msg: "Port is {{ http_port }}"The more specific location wins. Official precedence has 22 levels, but this simplified ladder covers day-to-day work.
ansible web1 -m setup -a "filter=ansible_distribution*" to show facts.PAUSE[Take a short break]
Halfway there. Before you continue, run docker ps and ansible all -m ping.
TASK[Playbook structure]
YAML in five minutes
--- # optional document start
name: Alice # key: value (a mapping)
is_admin: true # booleans: true / false
packages: # a list
- nginx
- curl
user: # a nested mapping
name: bob
shell: /bin/bash
inline_list: [a, b, c]
message: "Quote values that contain {{ vars }} or colons"
script: | # keep line breaks
line one
line two- Spaces only, never tabs.
- Indent consistently, two spaces is the norm.
-starts a list item.- Put a space after the colon:
key: value. - Quote any value that starts with
{{. - Check your work with
--syntax-checkoransible-lint.
Playbook, play, task, module
- Playbook
- A YAML file holding an ordered list of plays.
- Play
- Targets a group of hosts and sets options such as
become,varsandgather_facts. - Task
- One step. It runs on every host in the play before the next task starts.
- Module
- The code that does the work for a task.
Anatomy of a playbook
---
- name: Install and configure nginx # play name
hosts: webservers # which inventory group
become: true # run as root via sudo
vars:
owner_team: "Ansible learners"
tasks:
- name: Install nginx
ansible.builtin.apt: # module, written with its full name
name: nginx # module arguments describe the state
state: present
update_cache: true
- name: Deploy the home page from a template
ansible.builtin.template:
src: templates/index.html.j2
dest: /var/www/html/index.html
mode: "0644"
notify: Restart nginx # triggers the handler below
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
handlers:
- name: Restart nginx # runs once, at the end, only if notified
ansible.builtin.service:
name: nginx
state: restarted$ ansible-playbook playbooks/03_webserver.yml --check # dry run
$ ansible-playbook playbooks/03_webserver.yml # for real
$ ansible-playbook playbooks/03_webserver.yml # again: changed=0Open localhost:8081 and localhost:8082 in your browser. Then change page_title in inventory/group_vars/webservers.yml and run with --diff: only the template changes, so the handler fires.
Loops, conditionals and register
- name: Install packages # loop: repeat for each item
ansible.builtin.apt:
name: "{{ item }}"
loop: [curl, tree, vim-tiny]
- name: Only on Debian-family systems # when: run only if true
ansible.builtin.debug:
msg: "apt is the right module here"
when: ansible_facts['os_family'] == "Debian"
- name: Read uptime # register: save the result
ansible.builtin.command: uptime
register: up
changed_when: false # reading never changes the server
- name: Show it
ansible.builtin.debug:
var: up.stdoutwhen takes a Jinja2 expression without the curly braces. changed_when: false keeps the report honest for commands that only read data.
Roles: organise playbooks for reuse
ansible-lab/
βββ ansible.cfg
βββ inventory/
β βββ wsl.ini
β βββ group_vars/
β βββ host_vars/
βββ playbooks/site.yml
βββ roles/
βββ nginx/
βββ tasks/main.yml
βββ handlers/main.yml
βββ templates/index.html.j2
βββ files/
βββ vars/main.yml
βββ defaults/main.yml
βββ meta/main.ymltasks/- The entry point,
main.yml. handlers/- Restart and reload handlers.
templates/- Jinja2
.j2files. files/- Static files for
copy. defaults/- Lowest-priority variables.
meta/- Author and dependencies.
- hosts: webservers
become: true
roles:
- nginxCreate a skeleton with ansible-galaxy role init roles/myrole. Try ansible-playbook playbooks/site.yml --tags motd to run only part of a role.
Reading the output
when condition was false.PLAY RECAP *********************************************
web1 : ok=5 changed=0 unreachable=0 failed=0 skipped=0
web2 : ok=6 changed=3 unreachable=0 failed=0 skipped=0Useful flags: --syntax-check, --check, --diff, --limit web1, --tags motd, --list-tasks and -v up to -vvvv.
lineinfile task that removes nginx's IPv6 listener, because some Docker setups have no IPv6.TASK[Authentication mechanism]
Authentication is really three separate questions.
- 1. How do I log in?
- SSH password (
-k, needssshpass) or, preferably, an SSH key pair. Windows uses WinRM (NTLM, Kerberos or certificates) or SSH.ansible_userorremote_usersets the login name. - 2. How do I get root?
- Privilege escalation with
become: true(sudo by default),become_userto switch to another account, and-Kto prompt for the sudo password. - 3. How do I protect secrets?
- Ansible Vault encrypts files with AES-256.
no_log: truehides values in output. Larger teams often use HashiCorp Vault or CyberArk.
From password to SSH keys
- Create a key pair on the control node
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" - First contact with a password
ansible all -m ping -kand typeansible - Copy the public key to every node
ansible-playbook playbooks/06_ssh_keys.yml -k - Log in without a password from now on
ansible all -m ping
- name: Distribute the control node's public key
hosts: all
gather_facts: false
tasks:
- name: Add public key to ~ansible/.ssh/authorized_keys
ansible.posix.authorized_key:
user: ansible
state: present
key: "{{ lookup('ansible.builtin.file', lookup('ansible.builtin.env', 'HOME') + '/.ssh/id_ed25519.pub') }}"The private key never leaves the control node. Only the public key is copied to the servers.
Privilege escalation
- hosts: dbservers
become: true # the whole play runs as root
tasks:
- name: Create a database as the postgres user
ansible.builtin.command: createdb app
become_user: postgres # just this taskOnly escalate where you need to. Ad-hoc equivalents are -b to become root and -K to be asked for the sudo password.
Ansible Vault
$ cp vault/secrets.example.yml vault/secrets.yml
$ ansible-vault encrypt vault/secrets.yml # choose a vault password
$ cat vault/secrets.yml # $ANSIBLE_VAULT;1.1;AES256 ...
$ ansible-vault view vault/secrets.yml
$ ansible-vault edit vault/secrets.yml
$ ansible-playbook playbooks/07_vault.yml --ask-vault-passNever commit plain-text passwords. Commit the encrypted file and share the vault password through a separate channel. The lab's .gitignore already excludes vault/secrets.yml.
TASK[Executing Ansible modules]
An ad-hoc command runs a single module from the terminal, with no playbook.
ansiblethe commandwebservershost pattern-bbecome root-m aptmodule-a "name=tree state=present"module argumentsUse ad-hoc for
- Quick checks: ping, uptime, disk space
- One-off fixes, like restarting a service
- Gathering facts from many hosts
- Trying a module before writing a task
Use a playbook for
- Anything repeatable or multi-step
- Changes that should be reviewed in Git
- Handlers, templates and roles
- Documenting how a server is built
$ ansible all -m ansible.builtin.command -a "hostname"
$ ansible all -m ansible.builtin.shell -a "df -h | grep /"
$ ansible web1 -m ansible.builtin.setup -a "filter=ansible_distribution*"
$ ansible webservers -b -m ansible.builtin.apt -a "name=tree state=present update_cache=true"
$ ansible all -b -m ansible.builtin.copy -a "content='hello' dest=/tmp/hello.txt"
$ ansible dbservers -b -m ansible.builtin.user -a "name=carol state=present"
$ ansible webservers -b -m ansible.builtin.service -a "name=nginx state=restarted"
$ ansible 'webservers:!web2' -m ping # all web servers except web2Run the apt command twice. The first run reports changed, the second ok. That is idempotency. More commands are in CHEATSHEET.md.
command, shell or raw?
| Module | What it does | Good for | Watch out |
|---|---|---|---|
command | Runs a program directly, no shell. The default module. | Simple commands; safe from shell injection | No pipes, redirects, $VARS or && |
shell | Runs through /bin/sh on the node. | Pipes, redirects, environment variables | Easy to write tasks that are not idempotent |
raw | Sends a plain SSH command; needs no Python. | Installing Python on a new host; network devices | No state checking at all |
Prefer a dedicated module (apt, copy, service) over all three. If you must run a command, prefer command, and add creates, removes or changed_when so the task reports honestly.
-b on package installs (permission errors) and for quoting mistakes in -a. The default module is command, so ansible all -a uptime also works.TASK[Modules]
- Packages
apt,dnf,yum,package,pip- Files
copy,file,template,lineinfile,fetch- Services
service,systemd,cron- Users and security
user,group,authorized_key,firewalld- Commands
command,shell,script,raw- Source and network
git,get_url,uri,unarchive- Cloud
amazon.aws,azure.azcollection,google.cloudcollections- Windows
win_package,win_service,win_copy
The ten you will use most
| Module | What it does | Example arguments |
|---|---|---|
ping | Tests the connection and Python | ansible all -m ping |
apt / dnf | Installs or removes packages | name=nginx state=present |
copy | Copies a file or writes content | src=app.conf dest=/etc/app.conf |
template | Renders a Jinja2 template | src=index.html.j2 dest=/var/www/html/index.html |
file | Creates folders and links; sets permissions | path=/opt/app state=directory mode=0755 |
lineinfile | Makes sure one line is in a file | path=/etc/hosts line="10.0.0.5 db1" |
service | Starts, stops, restarts and enables services | name=nginx state=started enabled=true |
user / group | Manages accounts | name=alice groups=trainees append=true |
command / shell | Runs commands, as a last resort | cmd: uptime |
debug | Prints messages and variables | var: result.stdout |
Look modules up instead of memorising them
$ ansible-doc -l | grep -i apt # list and search modules
$ ansible-doc ansible.builtin.copy # full documentation with examples
$ ansible-doc -s ansible.builtin.user # short snippet to paste into a task
$ ansible-galaxy collection list # installed collections
$ ansible-galaxy collection install community.generalAlways write the fully qualified collection name (FQCN): ansible.builtin.copy rather than copy. It reads as namespace ansible, collection builtin, module copy. It avoids clashes between collections, and ansible-lint flags short names. Full docs are at docs.ansible.com.
ansible-doc page is the most useful part for beginners. For every module, state is the key argument: present or absent, started, stopped or restarted, directory, file or absent.TASK[Best practices]
- Name every task
- Clear names make output and failures easy to read.
- Modules over shell
- Dedicated modules are idempotent; shell commands rarely are.
- Use FQCN
ansible.builtin.copy, never a barecopy.- Structure with roles
- Small roles with one job each, reused across playbooks.
- Separate data from code
- Values in
group_varsandhost_vars; logic in tasks. - Keep secrets in Vault
- Never commit plain-text passwords or keys.
- Check before you change
--syntax-check,--check --diffandansible-lint.- Least privilege
- Use
becomeonly on tasks that need root. - Version everything
- Git, code review, and CI that runs
ansible-lint.
Same goal, better task
Avoid
- shell: apt-get install -y nginx
- shell: echo "ServerName x" >> /etc/app.conf
- copy: src=db.conf dest=/etc/
# the password is inside db.conf, in Git
- hosts: all
become: true # every task as rootPrefer
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Set ServerName once
ansible.builtin.lineinfile:
path: /etc/app.conf
line: "ServerName x"
# secrets in Vault, become per taskThe echo >> version appends a new line every time it runs. Run it five times and you get five lines. The lineinfile version is idempotent.
$ ansible-lint playbooks rolesThe lab project passes the strictest production profile, so it is a good reference for your own work.
TASK[Recap and quiz]
- Ansible
- Agentless, YAML-based, push-style automation over SSH or WinRM.
- Versus others
- Chef and Puppet use agents and pull; Salt uses an event bus.
- Architecture
- Control node with inventory, playbooks and modules, connecting to managed nodes.
- Playbooks
- Playbook, plays, tasks, modules; plus handlers, variables and roles.
- Authentication
- SSH keys to connect,
becomefor root, Vault for secrets. - Modules
- Ad-hoc for one-offs; dedicated modules over shell; look them up with
ansible-doc.
Quick quiz
What makes Ansible agentless?
It connects over SSH or WinRM, copies small modules, runs them and cleans up. Nothing stays running on the managed node.
What does idempotent mean?
Running the same task again produces the same end state, and reports no change if nothing drifted.
What is the difference between a play and a task?
A play maps a group of hosts to a list of tasks. A task is a single call to one module.
When does a handler run?
Once, at the end of the play, and only if a task that notifies it reported changed.
Which module adds one line to a file safely?
ansible.builtin.lineinfile.
Why prefer SSH keys over passwords?
No password is sent or stored, logins can be automated safely, and keys can be revoked per machine.
Keep going after today
- Finish all seven exercises in EXERCISES.md, plus the stretch goals.
- Read the getting-started and playbook guides at docs.ansible.com.
- Browse roles and collections on galaxy.ansible.com.
- Automate one real chore: set up your own development machine with a playbook.
Stop the lab when you are done with docker compose stop, and start it again any time with docker compose start.
TASK[Appendix: set up the lab on Windows]
You need Windows 10 22H2 or Windows 11, 8 GB of RAM (16 GB is comfortable), 15 GB of free disk, virtualization turned on in the BIOS, admin rights and internet access.
- Check the prerequisitesIn PowerShell, from the lab folder:
powershell -ExecutionPolicy Bypass -File .\scripts\check-prereqs.ps1 - Install WSL2 with UbuntuPowerShell as Administrator:
wsl --install -d Ubuntu-24.04, reboot, then create your Linux user. - Install Docker DesktopKeep the WSL 2 backend. In Settings, Resources, WSL integration, turn on Ubuntu-24.04. Test in Ubuntu with
docker run --rm hello-world. - Install VS Code (recommended)Add the WSL, Ansible and YAML extensions, then run
code .from the lab folder. - Get the lab into your Linux home folder
git clone https://github.com/YOUR-USER/ansible-lab.git ~/ansible-lab
Do not work from/mnt/c/β¦: Windows folders look world-writable to Linux, so Ansible ignoresansible.cfgthere. - Install Ansible
cd ~/ansible-lab && bash scripts/setup-wsl.sh && source ~/.bashrc - Start the servers
docker compose up -d --build, thendocker psshould list web1, web2, db1 and control. - Smoke test
ansible all -m ping -kwith the passwordansible. Three green lines and you are done.
Can't install WSL? Run docker compose up -d --build in PowerShell, then docker compose exec control bash. That container already has Ansible and works from /ansible.
Common problems
| What you see | Fix |
|---|---|
ansible: command not found | Run source ~/.bashrc or open a new terminal. |
| βworld writable directory β¦ ignoring ansible.cfgβ | You are in /mnt/c. Move the lab to ~. |
| βyou must install the sshpass programβ | sudo apt install -y sshpass |
UNREACHABLE β¦ Connection refused | The containers are not running: docker compose up -d. |
docker: command not found in WSL | Turn on WSL integration for Ubuntu in Docker Desktop settings. |
Permission denied (publickey,password) | Use -k with password ansible, or rerun 06_ssh_keys.yml -k after a lab reset. |
| Downloads fail on the office network | Set the proxy in Docker Desktop, and export https_proxy=http://proxy:port in WSL. |