Ansible Roles, Variables & Idempotency
Write a role that configures a server, then prove the second run changes nothing.
- Time
- 50 min
- Level
- Intermediate
- Objectives
- 4 objectives
- Cost
- Free
Where this fits in the platform
This lab adds
- Roles whose second run reports no changes at all
Which lets you
Before you start
You will need
- Ansible 2.15+
- A target host or container
You do not need these already — the lab environment below provides them.
You will be able to
- Structure a role so callers can override what they should
- Write tasks that report changed only when something changed
- Use handlers so a restart happens once, not per task
Cost — Free
— target a local container or VM.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
The playbook works. Running it twice restarts production, because every task reports changed whether or not anything changed.
Idempotency is the property that makes configuration management safe to run continuously, and it does not happen by accident.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
You will need:
- ansible
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
The role layout
Step 1 of 5
What you are proving: You can lay out a role so Ansible finds each part by name rather than by configuration
This step settles no success criterion on its own.
roles/webserver/
tasks/main.yml what to do
handlers/main.yml things triggered by notify
defaults/main.yml variables the caller may override
vars/main.yml variables the caller should not
templates/ Jinja2 rendered onto the host
files/ copied verbatim
meta/main.yml dependenciesAnsible loads these by name — putting a file in the right directory is the wiring.
defaults/ versus vars/ is the decision that makes a role reusable. Both
define variables; they differ in precedence. defaults sits near the bottom, so
inventory, playbook and --extra-vars all override it. vars sits near the top
and is effectively unoverridable.
Every variable a caller might reasonably change belongs in defaults. Put it in
vars and you have written a role only you can use.
What you are proving: You can write tasks that take their values from variables, with nothing hardcoded
Marking this settles success criterion 1.
# roles/webserver/tasks/main.yml
- name: Install nginx
ansible.builtin.package:
name: "{{ webserver_package }}"
state: present
- name: Deploy configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
mode: "0644"
validate: "nginx -t -c %s" # refuse to install a broken config
notify: Restart nginx
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: trueEvery module here checks state before acting. package does nothing if it is
installed; template compares a checksum and rewrites only on difference.
validate: is the detail worth copying — Ansible renders to a temporary file,
runs nginx -t against it, and only installs it if it parses. A broken template
can never take the service down.
What you are proving: You can restart a service exactly once at the end, through a handler rather than inline
Marking this settles success criterion 3.
# roles/webserver/handlers/main.yml
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restartedThree tasks can all notify this and nginx restarts once, after everything
has converged. Restarting inline per task would bounce the service repeatedly
during a single run.
What you are proving: You can demonstrate changed=0 on a second run, rather than assuming it
Marking this settles success criterion 2.
ansible-playbook -i inventory site.yml
# PLAY RECAP: ok=4 changed=3
ansible-playbook -i inventory site.yml
# PLAY RECAP: ok=4 changed=0 <- the point of the whole labchanged=0 on the second run is the definition of idempotent. If it is not
zero, something reports change every time, and you can find it:
ansible-playbook -i inventory site.yml --check --diff--check is a dry run; --diff shows exactly what it wants to alter.
What you are proving: You can name two ways a task silently breaks idempotency, and guard against them
Marking this settles success criterion 4.
command and shell always report changed. They have no idea what state
they produce, so Ansible assumes the worst:
# breaks idempotency
- name: Extract archive
ansible.builtin.shell: tar xzf /tmp/app.tar.gz -C /opt/app
# fixed — a guard that tells Ansible when the work is already done
- name: Extract archive
ansible.builtin.unarchive:
src: /tmp/app.tar.gz
dest: /opt/app
remote_src: true
creates: /opt/app/bin/startA template that renders differently each run. A timestamp or a random value in the template makes the checksum differ every time, so it rewrites and notifies a restart forever. Anything genuinely dynamic belongs outside the managed file.
The second run still reports changed
--check --diff names the task. Almost always a shell/command without creates, or a template with dynamic content.
The handler never runs
Handlers run at the end of a play, and are skipped entirely if the notifying task did not change. Force with --force-handlers when debugging.
Overriding a variable has no effect
It is in vars/ rather than defaults/. vars outranks nearly everything a caller can set.
A bad template took the service down
Add validate: to the template task so a config that does not parse is never installed.
Maintained by others, on Killercoda. Useful for extra repetition on one tool — it does not complete this lab or settle any criterion above.
- Ansible Handsonwriting and developing playbooks
Success criteria
0 of 4
The concept behind it
Next up
Lab 30 of 59 on the project path
Previous: Ansible Architecture, Configuration & Automated Inventory