Automate a Dev Environment with Ansible

Write Ansible playbooks to automate a developer workstation setup.

Introduction

30 Second Summary

Every time you set up a new machine or reinstall your OS, you end up running the same commands over and over. Install this package, create that folder, tweak that config file. It is tedious, error-prone, and easy to forget a step.

In this project, you will build an Ansible playbook that automates developer workstation setup on your local Ubuntu machine. You will declare what you want the system to look like in a simple file, and Ansible will make it happen.

What You'll Build

A complete Ansible project that installs developer tools, creates a workspace directory structure, deploys a customized Git configuration from a template, and proves that running it twice produces zero changes.

By the end of this project, you'll have:

  • A working Ansible installation with a localhost inventory that skips SSH and runs automation directly on your machine.
  • A multi-task playbook using modules to install packages, create directories, and deploy a Jinja2 template as a config file.
  • Proven idempotency by running the playbook multiple times and observing zero drift on subsequent runs.
  • Secret Mission: Add a second play that audits your system and generates a dynamic HTML report of everything Ansible configured.

Are there any prerequisites?

You should be comfortable with basic Linux command line tasks like navigating directories, editing files, and running commands. No prior Ansible or automation experience is needed.

Install Ansible and Set Up the Project

Every time you set up a new workstation, you repeat the same manual steps: installing tools, creating folders, configuring Git. It is tedious and error-prone.

In this step, you will install Ansible and create a proper project structure. This gives you the foundation to automate all that manual work in the steps ahead.

In this step, get ready to:
  • Install Ansible on your Ubuntu machine using pipx.
  • Create the project directory with configuration files and folders.
  • Verify Ansible can communicate with localhost.
Install Ansible using pipx

On modern Ubuntu, the system Python is managed by the OS. Installing Python tools directly with pip causes conflicts. The recommended approach is to use pipx, which installs each tool in its own isolated environment.

  • Install pipx by running this command:
sudo apt update && sudo apt install pipx -y
  • Ensure the pipx binary directory is in your PATH by running:
pipx ensurepath
  • Apply the PATH changes to your current terminal session by running:
source ~/.bashrc

Why these three commands?

The first command installs pipx from Ubuntu's package manager. The second command (pipx ensurepath) adds ~/.local/bin to your PATH so your terminal can find tools that pipx installs. The third command reloads your shell config so the PATH change takes effect immediately.

  • Install ansible-core using pipx by running:
pipx install ansible-core

Why ansible-core and not the full ansible package?

The ansible-core package includes all the built-in modules you need for this project (apt, file, template, debug, shell). The full ansible package adds hundreds of community collections that are not needed here. Using ansible-core keeps installation fast and lightweight.

  • Verify the installation succeeded by running:
ansible --version

You should see output starting with the ansible-core version number, followed by details like the Python version and config file path.

Seeing "command not found"?

  • Make sure you ran pipx ensurepath and then source ~/.bashrc in the same terminal session.
  • If it still does not work, close your terminal entirely and open a fresh one. Then try ansible --version again.
  • On some Debian-based systems you may also need python3-venv installed. Run sudo apt install python3-venv and retry.

Need more help? Help me troubleshoot my Ansible installation on Ubuntu

Create the project directory structure

Ansible projects follow a standard layout with configuration files, an inventory, and directories for templates and variables. Setting this up now keeps everything organized.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ansible-dev-env
cd ansible-dev-env
  • Create the subdirectories for templates and group variables by running:
mkdir group_vars templates
  • Confirm the directories exist by running:
ls

You should see group_vars and templates in the output.

Now you will create the two configuration files that every Ansible project needs.

  • Create the Ansible configuration file by running:
nano ansible.cfg
  • Paste the following content into nano:
[defaults]
inventory = inventory.ini
host_key_checking = False

What does this config do?

  • inventory = inventory.ini tells Ansible where to find your list of target machines. You will create this file next.
  • host_key_checking = False disables SSH host key verification. Since you are running everything locally, this avoids unnecessary prompts.
  • Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit nano.
  • Create the inventory file by running:
nano inventory.ini
  • Paste the following content into nano:
[local]
localhost ansible_connection=local

What is an inventory file?

An inventory tells Ansible which machines to manage. Here, [local] defines a group name, and localhost is the machine inside that group.

The ansible_connection=local setting tells Ansible to skip SSH and execute commands directly on your machine. In production, you would list remote servers here instead.

  • Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit nano.
  • Confirm both files were created by running:
ls

You should see ansible.cfg, group_vars, inventory.ini, and templates in the output.

✔️ Awesome, I've got everything!

Great. Make sure you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is what each file should contain:

[defaults]
inventory = inventory.ini
host_key_checking = False
[local]
localhost ansible_connection=local

Nano showing a blank file?

  • If you accidentally saved an empty file, open it again with nano ansible.cfg and paste the content in.
  • Make sure you are inside the ansible-dev-env folder. Run pwd to confirm your location.

Still stuck? Help me set up my Ansible project files

Test connectivity to localhost

Before writing any playbooks, you should confirm Ansible can reach your target machine. The ping module is Ansible's built-in connectivity test.

  • Test that Ansible can connect to localhost by running:
ansible localhost -m ping

You should see a green SUCCESS response with "ping": "pong" in the output.

What is an ad-hoc command?

The command you just ran is an Ansible ad-hoc command. It runs a single module against a target without needing a full playbook. The -m ping flag tells Ansible to use the ping module, which checks connectivity and returns pong on success.

Ad-hoc commands are useful for quick one-off checks. For repeatable automation, you will write playbooks in the next step.

Seeing a warning or error instead of pong?

  • If you see a Python interpreter warning, that is safe to ignore. The ping should still succeed.
  • If you see a host not found error, make sure your ansible.cfg and inventory.ini are both in your current directory. Run ls to confirm.
  • If you see a permission error, check that ansible_connection=local is set correctly in inventory.ini. This setting bypasses SSH entirely.

Need help? Help me fix my Ansible localhost ping

Your Ansible installation is working and your project structure is ready. Next up, you will write your first playbook and use it to install developer tools automatically.

Write Your First Playbook with Package Management

In the previous step, you installed Ansible and confirmed it can talk to your local machine. Now it is time to put Ansible to real work.

A playbook is a YAML file that describes the desired state of your system. Instead of running install commands one by one, you declare what packages should be present and Ansible handles the rest.

In this step, get ready to:
  • Create a playbook that installs developer tools on your machine.
  • Run the playbook and watch Ansible install packages.
  • Prove idempotency by running the same playbook again with zero changes.
Create the playbook and define variables

Your playbook will reference a variable called dev_packages that holds the list of tools to install. Ansible reads variables from files inside a group_vars/ directory automatically.

  • Create the variables file by opening it in nano:
nano group_vars/all.yml
  • Add the following content to define your package list:
---
dev_packages:
  - git
  - curl
  - tree
  - htop
  • Save the file by pressing Ctrl+O, then Enter to confirm the filename, then Ctrl+X to exit nano.

What does this file do?

The group_vars/all.yml file defines variables that apply to all hosts in your inventory. Ansible loads these automatically when running a playbook.

The dev_packages variable is a list of four packages. Your playbook will loop through this list and ensure each one is installed.

  • Confirm the file was created by running:
cat group_vars/all.yml

You should see the YAML content you just typed, listing git, curl, tree, and htop.

Seeing an empty file or an error?

If cat shows nothing, you may have exited nano without saving. Re-open the file with nano group_vars/all.yml and make sure to press Ctrl+O before Ctrl+X.

If you see a "No such file or directory" error, make sure the group_vars/ directory exists. You created it in Step 1.

help me fix my group_vars/all.yml file

Now you will create the playbook itself. This is the main file that tells Ansible what to do.

  • Open a new file for your playbook:
nano playbook.yml
  • Add the following content to define your play and tasks:
---
- name: Configure developer workstation
  hosts: local
  become: true
  gather_facts: true

  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install developer packages
      ansible.builtin.apt:
        name: "{{ dev_packages }}"
        state: present
  • Save and exit nano by pressing Ctrl+O, Enter, then Ctrl+X.

What does this playbook do?

  • hosts: local targets the [local] group from your inventory file (which contains localhost).
  • become: true tells Ansible to use privilege escalation (like sudo) so it can install packages.
  • gather_facts: true collects system information (OS, IP, username) that variables like ansible_env.HOME rely on.
  • The first task refreshes the apt package cache, but only if it is older than 3600 seconds (one hour). This avoids unnecessary network calls on re-runs.
  • The second task reads dev_packages from your variables file and ensures every package in that list is installed.

You will test this playbook in the next substep. The real verification is seeing Ansible install the packages successfully.

YAML indentation errors?

YAML is whitespace-sensitive. Every indentation level uses exactly two spaces. Tabs are not allowed. If you get a syntax error when running the playbook, check that your indentation matches the code above exactly.

help me fix a YAML syntax error in my Ansible playbook

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete playbook.yml for comparison:

---
- name: Configure developer workstation
  hosts: local
  become: true
  gather_facts: true

  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install developer packages
      ansible.builtin.apt:
        name: "{{ dev_packages }}"
        state: present

And here is group_vars/all.yml:

---
dev_packages:
  - git
  - curl
  - tree
  - htop
Run the playbook and install packages

Your playbook is ready. Time to let Ansible do its job and install the four developer packages on your system.

  • Run the playbook by executing this command:
ansible-playbook playbook.yml

What should I see?

Ansible prints each task name as it runs. You will see output like:

  • PLAY [Configure developer workstation] at the top.
  • Each task showing changed in yellow text (meaning Ansible modified the system).
  • A PLAY RECAP at the bottom showing changed=2 (one for the cache update, one for the package install).

The changed status means Ansible took action to bring your system into the desired state. This is exactly what you want on a first run.

Seeing a permission error?

If you see a message about permissions or "become" failing, your user may need a passwordless sudo configuration. Try running the playbook with the --ask-become-pass flag and enter your password when prompted:

ansible-playbook playbook.yml --ask-become-pass

help me fix an Ansible permission or become error

Run again to prove idempotency

One of Ansible's most powerful properties is idempotency. This means running the same playbook twice produces the same end state without making unnecessary changes.

  • Run the exact same command again:
ansible-playbook playbook.yml

This time, the PLAY RECAP should show changed=0. Every task reports ok in green because the system already matches the desired state.

Why does idempotency matter?

In production, you run playbooks repeatedly (on schedule, after deploys, during drift checks). If a playbook made changes every time, you would never know whether something actually needed fixing or was just being re-applied.

Idempotency guarantees that "nothing changed" means "everything is already correct." This is what makes Ansible safe to run on live servers.

Still seeing changed tasks on the second run?

If the apt cache update task shows changed on re-runs, check that you included cache_valid_time: 3600 in your playbook. Without it, Ansible refreshes the cache every single time.

help me understand why my playbook shows changed on the second run

Your playbook works and you have proven it is idempotent. Next up, you will add variables and directory management to build out a full workspace structure.

Add Variables and Directory Management

Your playbook can install packages, but right now every value is hardcoded. If you wanted to change the list of packages or reuse these settings on a different machine, you would need to edit the playbook itself.

Ansible solves this by letting you separate data from logic. You define variables in their own files, and the playbook references them by name. In this step, you will create a variables file and use it to drive new tasks that build a workspace directory structure automatically.

In this step, get ready to:
  • Create a variables file that stores reusable configuration data.
  • Add directory-creation tasks to your playbook using a loop.
  • Prove idempotency by running the playbook twice and seeing zero changes on the second run.
Create the variables file

Ansible looks for a special folder called group_vars in your project directory. Any YAML file inside that folder automatically loads its variables for the matching inventory group. A file named all.yml applies its variables to every host in your inventory.

  • Create the variables file by running this command:
nano group_vars/all.yml
  • Paste the following content into the file:
---
dev_user: "{{ ansible_user_id }}"
dev_email: "developer@example.com"
dev_fullname: "Dev User"

workspace_base: "{{ ansible_env.HOME }}/workspace"

project_dirs:
  - personal
  - work
  - sandbox

dev_packages:
  - git
  - curl
  - tree
  - htop

What does this file do?

This file defines all the configuration data your playbook needs in one place.

  • dev_user uses the built-in ansible_user_id fact to automatically resolve to whatever username is running Ansible. This makes the playbook portable without hardcoding a name.
  • workspace_base uses the ansible_env.HOME fact to dynamically build the path to your home directory.
  • project_dirs is a list that the playbook will loop over to create multiple directories from a single task.
  • dev_packages is the same list of packages your playbook already references. Defining it here means you can add or remove packages without touching the playbook logic.
  • Save the file by pressing Ctrl+O then Enter, and exit nano by pressing Ctrl+X.
  • Confirm the file exists by running:
cat group_vars/all.yml

You should see all your variable definitions printed back to the terminal.

What are Ansible facts?

Values like ansible_user_id and ansible_env.HOME are called facts. Ansible gathers them automatically at the start of each play (that is what gather_facts: true enables). Facts describe the target system and let your playbooks adapt to any machine without hardcoded paths or usernames.

File is empty or missing?

Make sure you created the file inside the group_vars folder, not in the project root. The path should be group_vars/all.yml.

If nano did not save, try running nano group_vars/all.yml again and check that you pressed Ctrl+O (write out) before Ctrl+X (exit).

Help me troubleshoot my group_vars/all.yml file not being created correctly in my Ansible project

Add directory-creation tasks to the playbook

The ansible.builtin.file module manages files and directories on the target system. You give it a path, tell it the desired state (directory or file), and Ansible creates or adjusts it to match.

  • Open your playbook for editing by running:
nano playbook.yml
  • Find the last task in the file, which ends with state: present.
  • Add a blank line below it, then paste the following two new tasks:
    - name: Create workspace base directory
      ansible.builtin.file:
        path: "{{ workspace_base }}"
        state: directory
        owner: "{{ dev_user }}"
        mode: "0755"

    - name: Create project subdirectories
      ansible.builtin.file:
        path: "{{ workspace_base }}/{{ item }}"
        state: directory
        owner: "{{ dev_user }}"
        mode: "0755"
      loop: "{{ project_dirs }}"

What do these tasks do?

  • The first task creates the base ~/workspace directory. It pulls the path from workspace_base (which you defined in group_vars/all.yml) and sets ownership to your user with 0755 permissions.
  • The second task uses loop to iterate over the project_dirs list. For each item in the list (personal, work, sandbox), it creates a subdirectory inside the workspace.
  • The {{ item }} variable is a special Ansible keyword that holds the current value from the loop on each iteration.
  • Save the file by pressing Ctrl+O then Enter, and exit nano by pressing Ctrl+X.

You will not see a visible change yet. The next substep runs the playbook and confirms the directories are created.

Worried about indentation?

YAML is whitespace-sensitive. Each new task must start at exactly 4 spaces of indentation (aligned with the existing - name: lines above). If your indentation is off, Ansible will report a syntax error when you run the playbook in the next substep.

Help me fix YAML indentation errors in my Ansible playbook

✔️ Awesome, I've got everything!

Great. Make sure you have saved playbook.yml before moving on.

ⓧ I'd like to double check the full code

Here is the complete playbook.yml as it should look right now:

---
- name: Configure developer workstation
  hosts: local
  become: true
  gather_facts: true

  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install developer packages
      ansible.builtin.apt:
        name: "{{ dev_packages }}"
        state: present

    - name: Create workspace base directory
      ansible.builtin.file:
        path: "{{ workspace_base }}"
        state: directory
        owner: "{{ dev_user }}"
        mode: "0755"

    - name: Create project subdirectories
      ansible.builtin.file:
        path: "{{ workspace_base }}/{{ item }}"
        state: directory
        owner: "{{ dev_user }}"
        mode: "0755"
      loop: "{{ project_dirs }}"
Run the playbook and verify the directories

Time to see your variables and directory tasks in action. Running the playbook will pull values from group_vars/all.yml and use them to create your workspace structure.

  • Run the playbook by executing this command:
ansible-playbook playbook.yml

You should see output where the apt tasks report ok (packages already installed) and the new file tasks report changed (directories being created for the first time).

The PLAY RECAP line at the bottom should show changed with a count greater than zero.

Seeing a syntax error?

Check that your YAML indentation is consistent. Every - name: line under tasks: must start at exactly 4 spaces. Parameters below each task name must be indented at 6 spaces.

Also verify that group_vars/all.yml exists and has the correct variable names. A typo like workspace_bases instead of workspace_base will cause an undefined variable error.

Help me fix errors when running my Ansible playbook with variables and the file module

  • Verify the directories were created with correct permissions by running:
ls -la ~/workspace/

You should see three subdirectories listed: personal, work, and sandbox. Each one should show your username as the owner and drwxr-xr-x permissions (which corresponds to mode 0755).

  • Now run the playbook a second time to prove idempotency:
ansible-playbook playbook.yml

This time, every task should report ok and the PLAY RECAP should show changed=0.

What is idempotency?

Idempotency means running the same operation multiple times produces the same result as running it once. Ansible checks the current state of the system before making changes. If the directories already exist with the correct owner and permissions, Ansible skips the work and reports ok instead of changed.

This is what makes Ansible safe to run repeatedly. You will never accidentally create duplicate directories or overwrite permissions that are already correct.

Your playbook now pulls all its data from variables, and you have proven it can build a directory structure reliably. Next up, you will use Jinja2 templates to generate custom configuration files from those same variables.

Deploy Templated Config Files and Use Handlers

Your workspace directories are set up and your packages are installed. Now it's time to tackle a common automation challenge: deploying configuration files that need to be customized per user.

In this step, you'll create a Jinja2 template that generates a personalized .gitconfig file using the variables you defined earlier. You'll also learn about handlers, which let Ansible react to changes by running follow-up tasks only when something actually changed.

In this step, get ready to:
  • Create a Jinja2 template that generates a customized Git configuration file.
  • Add a handler and a template deployment task to your playbook.
  • Prove that handlers only fire when a change occurs by running the playbook twice.
Create the Jinja2 template for .gitconfig

A Jinja2 template is a text file with placeholders that Ansible fills in at runtime using your variables. Instead of hardcoding your name and email into a config file, the template pulls those values from group_vars/all.yml automatically.

  • Create and open the template file by running:
nano templates/gitconfig.j2
  • Paste the following content into the editor:
# Managed by Ansible - do not edit manually
[user]
    name = {{ dev_fullname }}
    email = {{ dev_email }}

[init]
    defaultBranch = main

[core]
    editor = nano

[color]
    ui = auto

[pull]
    rebase = false

What does this template do?

The {{ dev_fullname }} and {{ dev_email }} placeholders are Jinja2 expressions. When Ansible deploys this template, it replaces them with the actual values from your variables file.

The rest of the file sets sensible Git defaults: main as the default branch, nano as the editor, colored output enabled, and merge-based pulls. The comment at the top warns anyone editing the file that Ansible manages it.

  • Save the file by pressing Ctrl+O, then Enter to confirm the filename, then Ctrl+X to exit nano.
  • Verify the file was saved correctly by running:
cat templates/gitconfig.j2
  • You should see the template content with {{ dev_fullname }} and {{ dev_email }} still as raw placeholders (not yet replaced with values).

File is empty or missing?

Make sure you pressed Ctrl+O (the letter O, not zero) to write the file before exiting. If the file is missing entirely, check you ran nano from inside the ~/ansible-dev-env/ directory where the templates/ folder exists.

help me figure out why my template file didn't save correctly

Add the handler and template task to your playbook

Handlers are special tasks that only run when notified by another task that reports a change. This is perfect for actions like printing a confirmation or restarting a service. You only want those to happen when something actually changed, not every time the playbook runs.

  • Open playbook.yml in nano by running:
nano playbook.yml
  • Find the line that says gather_facts: true. Add an empty line below it, then paste the following handler section above the existing tasks: line:
  handlers:
    - name: Confirm gitconfig deployed
      ansible.builtin.debug:
        msg: "Git configuration has been updated for {{ dev_fullname }}"

What is a handler?

A handler is a task that sits idle until another task "notifies" it. The notify keyword on a task triggers the handler, but only when that task reports a change. If nothing changed, the handler stays silent.

Handlers run at the end of all tasks in the play, not immediately when notified. This prevents duplicate executions when multiple tasks notify the same handler.

  • Now scroll down to the end of the tasks: section (after the "Create project subdirectories" task). Add the following new task at the bottom:
    - name: Deploy git configuration
      ansible.builtin.template:
        src: templates/gitconfig.j2
        dest: "{{ ansible_env.HOME }}/.gitconfig"
        owner: "{{ dev_user }}"
        mode: "0644"
      become: false
      notify: Confirm gitconfig deployed

What does this task do?

  • ansible.builtin.template takes your Jinja2 file, fills in the variables, and writes the result to the destination path.
  • become: false overrides the play-level become: true so the file is owned by your user, not root. The .gitconfig belongs in your home directory with your ownership.
  • notify: Confirm gitconfig deployed tells Ansible to fire the handler if this task makes a change (creates or updates the file).
  • Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit nano.

Not sure where to place the handler?

The handlers: section must be at the same indentation level as tasks: (two spaces in from the play level). It goes between gather_facts: true and the tasks: line. If you get a YAML syntax error when running the playbook, double-check your indentation.

help me fix the placement of handlers in my Ansible playbook

✔️ Awesome, I've got everything!

Great. Make sure you've saved playbook.yml before moving on.

ⓧ I'd like to double check the full code

Here is the complete playbook.yml at this point:

---
- name: Configure developer workstation
  hosts: local
  become: true
  gather_facts: true

  handlers:
    - name: Confirm gitconfig deployed
      ansible.builtin.debug:
        msg: "Git configuration has been updated for {{ dev_fullname }}"

  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install developer packages
      ansible.builtin.apt:
        name: "{{ dev_packages }}"
        state: present

    - name: Create workspace base directory
      ansible.builtin.file:
        path: "{{ workspace_base }}"
        state: directory
        owner: "{{ dev_user }}"
        mode: "0755"

    - name: Create project subdirectories
      ansible.builtin.file:
        path: "{{ workspace_base }}/{{ item }}"
        state: directory
        owner: "{{ dev_user }}"
        mode: "0755"
      loop: "{{ project_dirs }}"

    - name: Deploy git configuration
      ansible.builtin.template:
        src: templates/gitconfig.j2
        dest: "{{ ansible_env.HOME }}/.gitconfig"
        owner: "{{ dev_user }}"
        mode: "0644"
      become: false
      notify: Confirm gitconfig deployed
Run the playbook and observe handler behaviour

Now you'll see the handler in action. On the first run, the template task creates a new file, which triggers the handler. On the second run, the file is already correct, so nothing changes and the handler stays silent.

  • Run the playbook by executing:
ansible-playbook playbook.yml
  • Watch the output. You should see the "Deploy git configuration" task report changed, followed by a RUNNING HANDLER section that prints the confirmation message.

What should I see?

Near the end of the output, you'll see a line starting with RUNNING HANDLER [Confirm gitconfig deployed] followed by a debug message containing your dev_fullname value. The play recap at the bottom shows changed=1 (or higher if other tasks also changed).

  • Verify the rendered file was deployed correctly by running:
cat ~/.gitconfig
  • You should see the template with actual values filled in. The name field shows Dev User and the email field shows developer@example.com (the values from your variables file).
  • Now run the playbook a second time to prove idempotency:
ansible-playbook playbook.yml
  • This time, the "Deploy git configuration" task reports ok instead of changed. The handler does NOT appear in the output because no change was made. The play recap shows changed=0.

Why did the handler not fire?

Ansible compared the rendered template against the existing ~/.gitconfig file. Since the content is identical, Ansible reports ok (no change). A handler only fires when the task that notifies it reports changed. This is the power of idempotency combined with event-driven handlers.

Handler fired on the second run?

If the handler fires every time, something is modifying the file between runs. Check that no other process is editing ~/.gitconfig. Also verify that the owner and mode in the task match the file's actual permissions by running ls -la ~/.gitconfig.

help me figure out why my Ansible handler keeps firing on every playbook run

You've just experienced the core loop of Ansible automation: declare what you want, let Ansible make it happen, and trust that re-running the same playbook changes nothing if everything is already correct. Next up, you'll take on the Secret Mission and build a dynamic system audit report.

Secret mission

Build a System Audit Report

Your playbook already automates setup. But how do you prove what it configured? In this mission, you'll add a second play that gathers live system facts, captures installed package versions, and renders a dynamic HTML audit report you can open in any browser.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local Ubuntu machine, so there are no ongoing cloud costs. Decide whether to keep your resources in place, pause them to come back later, or delete them entirely.

Resources you used:

  • Ansible project directory on your Desktop (contains ansible.cfg, inventory.ini, playbook.yml, group_vars/, templates/).
  • Workspace directory at ~/workspace/ (created by the playbook with subdirectories and audit report).
  • Modified ~/.gitconfig file (deployed by the template module).
  • Apt packages installed by the playbook (git, curl, tree, htop).
  • ansible-core installed via pipx.

Keep everything running

No action needed. Choose this if you plan to keep extending your playbook or experimenting with new Ansible modules.

  • Your Ansible project directory stays on your Desktop, ready to edit.
  • The ~/workspace/ directory and its subdirectories remain in place.
  • The apt packages (git, curl, tree, htop) stay installed as system utilities.
  • ansible-core remains available via pipx for future automation projects.

Pause - I'll come back to this later

Since everything is local files and installed packages, there is nothing to "pause" in the traditional sense. Your files stay on disk and the tools stay installed until you delete them.

  • Leave everything as-is. There are no running processes or services to stop.
  • When you come back, navigate to your Ansible project folder and run ansible-playbook playbook.yml to pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to clean up completely.

  • Delete the Ansible project directory by running:
  • Delete the workspace directory created by the playbook by running:
  • Remove the Ansible-managed .gitconfig by running:
  • Uninstall the apt packages the playbook installed by running:
  • Uninstall ansible-core by running:

Nice Work!

Nice Work!

You just built a reusable Ansible playbook that sets up an entire developer workstation in a single command. No more manual installs, no more forgotten config steps.

You've learned how to:

  • Write a multi-task playbook using apt, file, and template modules to install packages, create directories, and deploy dynamic config files.
  • Separate configuration into variables and Jinja2 templates for reusability, and use handlers for event-driven task execution.
  • Prove idempotency by running the same playbook multiple times and observing zero changes on repeated runs.
  • Secret Mission: Built a dynamic system audit report using gathered facts and the register keyword to capture command output.

Ready to quiz yourself?