Automate Your Dev Environment with Ansible

Write Ansible playbooks that configure a full dev environment in one command.

Introduction

30 Second Summary

Every time you set up a new computer, you spend hours installing tools, tweaking settings, and configuring everything from scratch. DevOps engineers solved this problem years ago with automation.

In this project, you will use Ansible to write playbooks that set up your entire development environment in a single command. No more manual installs.

What You'll Build

A local Ansible automation project running on WSL 2 that configures a development environment from scratch using Infrastructure as Code.

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

  • An Ansible inventory and playbook that installs development packages on your local machine automatically.
  • A playbook with variables and handlers that installs and configures Nginx as a web server.
  • A reusable Ansible role organized into a clean, shareable structure you can run on any fresh Ubuntu machine.
  • Secret Mission: Encrypt sensitive configuration with Ansible Vault for secure secret management.

Are there any prerequisites?

None. This project includes full setup instructions for every tool you need, including WSL 2, Python 3, and Ansible itself.

Set Up WSL 2 and Install Ansible

Ansible cannot run natively on Windows. To use it, you need a Linux environment. WSL 2 (Windows Subsystem for Linux) gives you a full Ubuntu terminal right inside Windows, which is exactly where Ansible will live.

In this step, you will enable WSL 2, install Ubuntu, and then install Ansible inside a Python virtual environment. Once this is done, your machine is ready to run playbooks.

In this step, get ready to:
  • Enable WSL 2 and configure Ubuntu on your Windows machine.
  • Install Python 3, pip, and the venv module inside WSL.
  • Install Ansible in a Python virtual environment.
Enable and configure WSL 2 with Ubuntu

WSL 2 runs a real Linux kernel inside Windows. The wsl --install command enables it and installs Ubuntu in a single step.

  • Press the Windows key to open your search bar.
  • Type PowerShell and select Run as administrator from the right panel.
  • Install WSL 2 and Ubuntu by running this command:
wsl --install

What does this command do?

  • Enables the Virtual Machine Platform Windows feature.
  • Downloads and installs the WSL 2 Linux kernel.
  • Sets WSL 2 as the default version.
  • Downloads and installs Ubuntu as the default Linux distribution.
  • Restart your computer when prompted. The install requires a reboot to finish enabling WSL 2.
  • After restarting, Ubuntu will launch automatically and ask you to create a username and password. Enter any username and password you like. This is your Linux account inside WSL.

Why does Ubuntu ask for a password?

This creates a dedicated Linux user account. You will use this password when running commands with sudo (which grants temporary admin privileges inside Linux).

  • Verify WSL 2 is running by entering this command in the Ubuntu terminal:
cat /etc/os-release

You should see output that includes Ubuntu and a version number like 24.04.

Don't see the Ubuntu terminal?

  • If the terminal did not open automatically after restart, press the Windows key, type Ubuntu, and open it from the search results.
  • If wsl --install showed help text instead of installing, it means WSL was partially set up already. Run wsl --install -d Ubuntu to install the Ubuntu distribution specifically.
  • Make sure you are running Windows 10 build 19041 or later, or Windows 11. Older builds do not support the simplified install command.

Help me troubleshoot my WSL 2 installation

Install Python 3 and pip inside WSL

Ansible is a Python application. You need Python 3, the pip package manager, and the venv module to install it properly.

  • Update your package lists and install Python 3 with pip and venv by running this command in your Ubuntu terminal:
sudo apt update && sudo apt install python3 python3-pip python3-venv -y

What does this command do?

  • sudo apt update refreshes the list of available packages so you get the latest versions.
  • sudo apt install python3 python3-pip python3-venv -y installs the Python interpreter, the pip package manager, and the virtual environment module. The -y flag skips confirmation prompts.
  • When prompted, enter the password you created during Ubuntu setup. The terminal will not show characters as you type. Press Enter when done.
  • Verify Python is installed by running:
python3 --version

You should see Python 3.12.x (the exact patch number may vary). Ubuntu 24.04 ships with Python 3.12.

Seeing permission errors?

  • Make sure you typed sudo at the start of the command. Without it, the system will not have permission to install packages.
  • If it asks for a password that you never set, close the Ubuntu terminal and reopen it. Ubuntu should prompt you to create a user on first launch.

Help me fix Python installation issues in WSL

Install Ansible using pip in a virtual environment

Installing Ansible in a virtual environment keeps it isolated from your system Python packages. This prevents conflicts and makes cleanup easy.

  • Create a virtual environment, activate it, and install Ansible by running these commands:
python3 -m venv ~/ansible-env
source ~/ansible-env/bin/activate
pip install ansible

What do these commands do?

  • python3 -m venv ~/ansible-env creates a virtual environment directory at ~/ansible-env. This is an isolated copy of Python where you can install packages without affecting the system.
  • source ~/ansible-env/bin/activate activates the environment. You will see (ansible-env) appear at the start of your terminal prompt.
  • pip install ansible installs the Ansible community package inside the virtual environment.
  • The install may take a minute or two while it downloads Ansible and its dependencies.
  • Verify Ansible is installed by running:
ansible --version

✔️ I see version info

You should see output starting with ansible [core 2.x.x] followed by configuration details. This confirms Ansible is ready to use.

ⓧ Command not found

  • Make sure the virtual environment is active. Your terminal prompt should start with (ansible-env). If not, activate it again by running:
source ~/ansible-env/bin/activate
  • If pip install ansible showed errors during install, check your internet connection and try running it again.

Reactivating after closing the terminal

Every time you open a new Ubuntu terminal session, you will need to reactivate the virtual environment with source ~/ansible-env/bin/activate before using Ansible commands.

Your environment is ready. Next up, you will create your first Ansible inventory and test connectivity with ad-hoc commands.

Create Your Inventory and Run Ad-Hoc Commands

Ansible needs to know which machines it should manage. You tell it by writing an inventory file that lists every host and how to connect to it.

In this step, you will create your project directory, write an inventory targeting your local machine, and run ad-hoc commands to confirm Ansible can reach it.

In this step, get ready to:
  • Create a project directory and inventory file.
  • Test connectivity with ad-hoc commands.
  • Gather system facts with the setup module.
Create your project directory and inventory file

Every Ansible project lives in its own directory. The first file you create is the inventory, which tells Ansible where to send commands.

  • Create your project directory and move into it by running these commands:
mkdir ~/ansible-lab && cd ~/ansible-lab
  • Confirm you are inside the new directory by running:
pwd

You should see /home/your-username/ansible-lab in the output.

  • Create the inventory file by running this command:
cat > inventory.ini << 'EOF'
[local]
localhost ansible_connection=local
EOF

What does this file do?

The inventory.ini file defines a group called [local] containing one host: localhost.

The ansible_connection=local setting tells Ansible to execute commands directly on your machine instead of trying to connect over SSH. This is how you run Ansible against your own computer.

  • Verify the file was created correctly by running:
cat inventory.ini

You should see the [local] group header and the localhost ansible_connection=local line printed in your terminal.

File looks empty or missing?

Make sure you ran the full cat > inventory.ini << 'EOF' command including the closing EOF line. If the file is missing, confirm you are in the ~/ansible-lab directory with pwd.

help me troubleshoot my inventory.ini file creation in WSL

✔️ Awesome, I've got everything!

Great. Your inventory is ready to go.

ⓧ I'd like to double check the full code

[local]
localhost ansible_connection=local
Test connectivity with ad-hoc commands

Ad-hoc commands let you run a single Ansible task without writing a full playbook. They are perfect for quick tests and one-off operations.

  • Test that Ansible can reach localhost by running this command:
ansible all -i inventory.ini -m ping

What does this command do?

  • ansible all targets every host in the inventory.
  • -i inventory.ini points to your inventory file.
  • -m ping runs the ping module, which verifies Ansible can connect and that Python is available on the target. It is not an ICMP network ping.

You should see output similar to this:

localhost | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

The "ping": "pong" response confirms Ansible can communicate with your local machine.

Seeing a connection error?

Make sure your Ansible virtual environment is still activated. You should see (ansible-env) at the start of your terminal prompt. If not, reactivate it with:

source ~/ansible-env/bin/activate

Also confirm you are running the command from inside ~/ansible-lab where your inventory.ini file lives.

help me fix my Ansible ping command failing against localhost

  • Run an arbitrary command on localhost to see Ansible execute real work:
ansible all -i inventory.ini -m command -a "uptime"

You should see your system's uptime printed in the output, confirming Ansible can execute commands on the target host.

Gather system facts using the setup module

Ansible can automatically collect detailed information about a host. These are called facts and include the operating system, memory, network interfaces, and more.

  • Gather all facts about your local machine by running:
ansible all -i inventory.ini -m ansible.builtin.setup

What is the setup module?

The ansible.builtin.setup module collects system information and returns it as JSON. Playbooks use these facts to make decisions, like installing different packages depending on the OS.

The long name ansible.builtin.setup is the Fully Qualified Collection Name (FQCN). Using FQCNs is the recommended best practice because it makes your automation explicit about which module it uses.

You should see a large block of JSON data scroll by. This is every fact Ansible knows about your machine.

  • Filter the facts to show only your distribution info by running:
ansible all -i inventory.ini -m ansible.builtin.setup -a "filter=ansible_distribution*"

You should see a short JSON block with your Ubuntu version and distribution details. This confirms facts gathering works and you can filter for exactly what you need.

Seeing a module not found error?

Verify Ansible is installed by running ansible --version. If the command is not found, reactivate your virtual environment with source ~/ansible-env/bin/activate.

help me fix a module not found error when running ansible.builtin.setup

Your inventory is ready and you have confirmed Ansible can reach your local machine. Next up, you will write your first playbook to automate package installation.

Write Your First Playbook

Ad-hoc commands are useful for quick one-off tasks, but they do not scale. Imagine running 10 separate commands every time you set up a new machine.

An Ansible playbook lets you define all those tasks in a single YAML file and run them with one command. In this step, you will write a playbook that installs your entire development toolkit automatically.

In this step, get ready to:
  • Create a playbook that installs development packages.
  • Run the playbook and verify your packages are installed.
  • Prove idempotency by running the playbook a second time.
Create the setup-dev.yml playbook

A playbook is a YAML file that describes the desired state of your system. Ansible reads the playbook and makes your system match that state.

  • Create a new file called setup-dev.yml in your ~/ansible-lab directory.
  • Paste the following content into the file:
---
- name: Set up development environment
  hosts: local
  connection: local
  become: true

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

    - name: Install development packages
      ansible.builtin.apt:
        name:
          - git
          - curl
          - tree
          - jq
          - htop
        state: present

What does this playbook do?

The top section defines how and where the playbook runs:

  • hosts: local targets the [local] group you defined in inventory.ini.
  • connection: local tells Ansible to skip SSH and execute directly on this machine.
  • become: true enables privilege escalation (sudo) so Ansible can install packages.

The tasks section defines what to do. The first task refreshes the apt package index. The second task installs five packages using the ansible.builtin.apt module.

Why use Fully Qualified Collection Names?

You could write just apt instead of ansible.builtin.apt, but modern Ansible best practice is to use the full name.

Fully Qualified Collection Names make playbooks explicit about which module is being used. This avoids conflicts when you install additional Ansible collections later.

  • Save setup-dev.yml.

YAML not saving correctly?

YAML is sensitive to indentation. Make sure you use spaces (not tabs) and that each level is indented by exactly two spaces.

Check that the dashes in the package list line up vertically. Each dash should be at the same indentation level.

Help me fix YAML indentation errors in my Ansible playbook.

Run the playbook

With your playbook saved, you can now execute it against your inventory. Ansible will read the YAML, connect to localhost, and run each task in order.

  • Make sure your virtual environment is active and you are in the ~/ansible-lab directory.
  • Run the playbook by executing this command:
ansible-playbook -i inventory.ini setup-dev.yml

What should I see?

Ansible prints each task name as it runs, followed by a status for each host. You should see output showing changed for tasks that modified your system.

At the bottom, the PLAY RECAP summary shows the total counts. Look for a line like:

localhost : ok=3 changed=2 unreachable=0 failed=0

The ok count includes all tasks that ran successfully. The changed count shows tasks that actually modified something on the system.

  • Verify the packages installed by running:
git --version && jq --version

You should see version numbers printed for both git and jq, confirming they were installed by your playbook.

Seeing permission denied or failed tasks?

Make sure become: true is in your playbook. Without it, Ansible cannot use sudo to install packages.

If you see a connection error, confirm that connection: local is set in the playbook. Without this, Ansible tries to SSH to localhost and may fail.

Help me debug why my Ansible playbook is failing with permission or connection errors.

Prove idempotency

Idempotency means running the same operation multiple times produces the same result. A well-written playbook only makes changes when the system does not already match the desired state.

  • Run the exact same playbook again:
ansible-playbook -i inventory.ini setup-dev.yml

What should I see this time?

This time, the PLAY RECAP should show changed=0 for all tasks. Ansible checked the system, found that every package was already installed, and made zero modifications.

This is the power of idempotency. You can run this playbook on a fresh machine or on one that is already configured. Either way, you end up in the same state.

You should see the PLAY RECAP line with changed=0, confirming Ansible detected no work needed.

Still seeing changed tasks on the second run?

If the Update apt cache task shows changed, check that you have cache_valid_time: 3600 set. This tells Ansible to skip the cache update if it ran within the last hour.

Help me understand why my Ansible playbook shows changes on the second run.

✔️ Awesome, I've got everything!

Great. Double check you have saved setup-dev.yml and that both playbook runs completed successfully.

ⓧ I'd like to double check the full code

Here is the complete setup-dev.yml file:

---
- name: Set up development environment
  hosts: local
  connection: local
  become: true

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

    - name: Install development packages
      ansible.builtin.apt:
        name:
          - git
          - curl
          - tree
          - jq
          - htop
        state: present

And your inventory.ini should still look like this:

[local]
localhost ansible_connection=local

You now have a playbook that installs your dev tools in one command and proves it only makes changes when needed. Next up, you will add variables and handlers to make your playbooks even more powerful.

Add Variables and Handlers

In the previous step, you wrote a playbook that installs development packages and proved that Ansible only makes changes when needed. Now you'll write a more advanced playbook that configures a web server using variables and handlers.

Variables let you define values once and reuse them throughout a playbook. Handlers are special tasks that only run when notified by another task, which is perfect for restarting a service after a configuration change.

In this step, get ready to:
  • Create a new playbook with variables for web server configuration.
  • Deploy a custom web page using Ansible's templating syntax.
  • Define a handler that restarts Nginx only when its config changes.
Create the webserver playbook with variables

Instead of hardcoding values throughout your playbook, you can define them once in a vars section and reference them with double curly braces. This makes your playbook easy to update and reuse across different environments.

  • Create a new file called webserver.yml in your ~/ansible-lab folder by running this command:
touch ~/ansible-lab/webserver.yml
  • Open webserver.yml in your text editor and paste the following code:
---
- name: Install and configure Nginx
  hosts: local
  connection: local
  become: true

  vars:
    http_port: 80
    server_name: mysite.local
    doc_root: /var/www/mysite

  tasks:
    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Create document root
      ansible.builtin.file:
        path: "{{ doc_root }}"
        state: directory
        mode: "0755"

What does this code do?

The vars section defines three variables: http_port, server_name, and doc_root. You reference these later with {{ variable_name }} syntax.

  • The first task installs Nginx (a popular web server) using the ansible.builtin.apt module.
  • The second task creates the directory where your website files will live, using the doc_root variable for the path.
  • Save webserver.yml.
  • Confirm the file exists by running:
ls ~/ansible-lab/webserver.yml

You should see webserver.yml listed in the output.

Don't see the file?

  • Make sure you saved the file in ~/ansible-lab/ and not somewhere else.
  • Check that the filename is exactly webserver.yml with no extra spaces or typos.

Why isn't my webserver.yml file showing up?

Add content deployment and the handler

Now you'll add tasks that deploy a custom web page and configure Nginx to serve it. The key concept here is the notify directive, which triggers a handler only when a task makes a change.

  • In webserver.yml, find the Create document root task you just wrote.
  • Add the following task directly below it (keeping the same indentation level as the other tasks):
    - name: Deploy custom index page
      ansible.builtin.copy:
        content: |
          <!DOCTYPE html>
          <html>
          <head><title>Welcome to {{ server_name }}</title></head>
          <body>
            <h1>Hello from {{ ansible_hostname }}!</h1>
            <p>This page was deployed by Ansible.</p>
            <p>Server: {{ server_name }} | Port: {{ http_port }}</p>
          </body>
          </html>
        dest: "{{ doc_root }}/index.html"
        mode: "0644"

What does this code do?

This task uses ansible.builtin.copy with a content parameter to write an HTML file directly from the playbook. The {{ ansible_hostname }} variable is a built-in Ansible fact that resolves to your machine's hostname at runtime.

Your custom variables (server_name and http_port) also appear in the HTML, so the deployed page reflects your configuration.

  • Below the Deploy custom index page task, add the remaining tasks and the handlers section:
    - name: Configure Nginx site
      ansible.builtin.copy:
        content: |
          server {
              listen {{ http_port }};
              server_name {{ server_name }};
              root {{ doc_root }};
              index index.html;

              location / {
                  try_files $uri $uri/ =404;
              }
          }
        dest: /etc/nginx/sites-available/mysite
        mode: "0644"
      notify: Restart Nginx

    - name: Enable site
      ansible.builtin.file:
        src: /etc/nginx/sites-available/mysite
        dest: /etc/nginx/sites-enabled/mysite
        state: link
      notify: Restart Nginx

    - name: Remove default site
      ansible.builtin.file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: Restart Nginx

    - name: Ensure Nginx is started
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Restart Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

How do handlers work?

The notify: Restart Nginx line on the configuration tasks tells Ansible to queue the handler named Restart Nginx. Handlers only fire once at the end of the play, even if multiple tasks notify them.

This means Nginx restarts only when a configuration file actually changes. If you run the playbook again and nothing changes, the handler is never triggered.

  • Save webserver.yml.

You won't see a change yet. The next substep runs the playbook to verify everything works.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete webserver.yml file:

---
- name: Install and configure Nginx
  hosts: local
  connection: local
  become: true

  vars:
    http_port: 80
    server_name: mysite.local
    doc_root: /var/www/mysite

  tasks:
    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Create document root
      ansible.builtin.file:
        path: "{{ doc_root }}"
        state: directory
        mode: "0755"

    - name: Deploy custom index page
      ansible.builtin.copy:
        content: |
          <!DOCTYPE html>
          <html>
          <head><title>Welcome to {{ server_name }}</title></head>
          <body>
            <h1>Hello from {{ ansible_hostname }}!</h1>
            <p>This page was deployed by Ansible.</p>
            <p>Server: {{ server_name }} | Port: {{ http_port }}</p>
          </body>
          </html>
        dest: "{{ doc_root }}/index.html"
        mode: "0644"

    - name: Configure Nginx site
      ansible.builtin.copy:
        content: |
          server {
              listen {{ http_port }};
              server_name {{ server_name }};
              root {{ doc_root }};
              index index.html;

              location / {
                  try_files $uri $uri/ =404;
              }
          }
        dest: /etc/nginx/sites-available/mysite
        mode: "0644"
      notify: Restart Nginx

    - name: Enable site
      ansible.builtin.file:
        src: /etc/nginx/sites-available/mysite
        dest: /etc/nginx/sites-enabled/mysite
        state: link
      notify: Restart Nginx

    - name: Remove default site
      ansible.builtin.file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: Restart Nginx

    - name: Ensure Nginx is started
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Restart Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted
Run the playbook and verify

Time to see your variables and handlers in action. Running this playbook will install Nginx, deploy your custom page, and configure the server in one command.

  • Make sure you are in the ~/ansible-lab directory by running:
cd ~/ansible-lab
  • Run the webserver playbook by executing this command:
ansible-playbook -i inventory.ini webserver.yml

You should see output showing each task completing with changed status. At the end, a RUNNING HANDLER line confirms that Restart Nginx was triggered by the configuration tasks.

What should I see?

The output shows a play recap at the bottom. Look for changed counts next to each task. On the first run, most tasks will show changed because Nginx is being installed and configured for the first time.

  • Verify your custom page is being served by running:
curl http://localhost

You should see your custom HTML page with the greeting "Hello from" followed by your machine's hostname.

  • Confirm Nginx is running and enabled by running:
systemctl status nginx

You should see active (running) in the output, confirming Nginx is serving your page.

Seeing a connection refused error?

  • Check that the playbook ran without errors. Look for red text in the output indicating a failed task.
  • Make sure your webserver.yml YAML indentation is correct. YAML is sensitive to spaces. Use the double-check tab above to compare.
  • Try running sudo systemctl start nginx manually to see if Nginx can start.

Why is curl showing connection refused after running my Ansible playbook?

Why is this better than a shell script?

A shell script would blindly re-install and re-configure everything on every run. Ansible checks the current state first and only makes changes when needed.

The handler pattern adds another layer of intelligence. Nginx only restarts when its config actually changes, avoiding unnecessary downtime.

Your web server is configured and serving a custom page, all from a single Ansible command. Next up, you'll organize your automation into reusable roles that you can share and version control.

Organize with Roles

Your playbooks work great, but they live as standalone files with tasks and variables mixed together. As your automation grows, flat playbook files become harder to share, version control, and reuse across projects.

Ansible roles solve this by organizing tasks, variables, and configuration into a standard directory structure that any Ansible user recognizes. In this step, you'll refactor your dev setup into a reusable role and run it from a clean master playbook.

In this step, get ready to:
  • Create a role scaffold with ansible-galaxy.
  • Move your dev setup tasks into the role structure.
  • Run the role-based playbook and verify it works identically.
Create a role structure with ansible-galaxy

Ansible Galaxy is the built-in tool for managing role scaffolds. It generates the standard directory layout that all Ansible projects use in production.

  • Create the role scaffold by running this command from your ~/ansible-lab directory:
ansible-galaxy init roles/devsetup

What does this command do?

The ansible-galaxy init command creates a standardized directory tree inside roles/devsetup/.

Each subdirectory has a specific purpose: tasks/ holds the automation logic, defaults/ holds default variable values, handlers/ holds event-triggered actions, and templates/ holds Jinja2 template files.

  • Explore the generated structure by running:
tree roles/devsetup

You should see subdirectories including tasks/, handlers/, defaults/, vars/, templates/, and files/, each with a main.yml inside.

Command not found for tree?

If tree is not available, your dev packages from Step 3 may not have installed correctly. Run ls -R roles/devsetup instead to see the structure.

help me troubleshoot why tree is not installed in my WSL environment

Move your tasks into the role

Now you'll populate the role with the same logic from your setup-dev.yml playbook, but organized into the role's standard locations.

  • Open roles/devsetup/defaults/main.yml in your editor and replace its contents with this code:
---
dev_packages:
  - git
  - curl
  - tree
  - jq
  - htop

What does this code do?

The defaults/ directory holds variable values that can be overridden by any playbook that uses this role. By defining dev_packages here, anyone can customize the package list without editing the role's task logic.

  • Save roles/devsetup/defaults/main.yml.
  • Open roles/devsetup/tasks/main.yml and replace its contents with this code:
---
- name: Update apt cache
  ansible.builtin.apt:
    update_cache: true
    cache_valid_time: 3600

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

What does this code do?

These are the same two tasks from your setup-dev.yml playbook, with one key difference. Instead of a hardcoded package list, the install task now references {{ dev_packages }} which pulls from the defaults file you just created.

This separation of data from logic is what makes roles reusable. Different projects can include the same role but override the package list to fit their needs.

  • Save roles/devsetup/tasks/main.yml.
  • Create a new file called site.yml in your ~/ansible-lab directory and add this code:
---
- name: Configure development environment
  hosts: local
  connection: local
  become: true

  roles:
    - devsetup

What does this code do?

This is your master playbook. Instead of listing individual tasks, it references the devsetup role by name. Ansible automatically looks in roles/devsetup/tasks/main.yml for the tasks and roles/devsetup/defaults/main.yml for the variables.

In production, a site.yml file often references many roles (web server, database, monitoring) to configure an entire machine in one run.

  • Save site.yml.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before running the playbook.

ⓧ I'd like to double check the full code

Here are all three files for comparison:

---
dev_packages:
  - git
  - curl
  - tree
  - jq
  - htop
---
- name: Update apt cache
  ansible.builtin.apt:
    update_cache: true
    cache_valid_time: 3600

- name: Install development packages
  ansible.builtin.apt:
    name: "{{ dev_packages }}"
    state: present
---
- name: Configure development environment
  hosts: local
  connection: local
  become: true

  roles:
    - devsetup
Run the role-based playbook

With your role in place, you can now execute the entire dev setup through the master playbook.

  • Run the role-based playbook by executing this command:
ansible-playbook -i inventory.ini site.yml

Since you already installed these packages in Step 3, every task should report ok with changed=0 at the end. This proves your role works correctly and demonstrates idempotency once more.

Why is changed=0 a good sign?

Ansible compares the desired state (packages installed) with the actual state of your system. Because those packages already exist, Ansible reports no changes needed.

If you ran this on a brand new machine, you would see changed=1 on the install task as it brought the system to the desired state for the first time.

Seeing an error about the role not being found?

Make sure you are running the command from inside ~/ansible-lab (the same directory that contains the roles/ folder). Ansible looks for roles relative to the playbook's location.

Also confirm the directory is named exactly roles/devsetup and that tasks/main.yml exists inside it.

help me troubleshoot why Ansible cannot find my devsetup role

Your automation is now structured like a professional Ansible project. Next up, you'll tackle a Secret Mission that adds encrypted secrets management to your toolkit.

Secret mission

Encrypt Secrets with Ansible Vault

Your playbooks can install packages and configure servers. But what about sensitive data like API keys and database passwords? In this secret mission, you will use Ansible Vault to encrypt secrets and deploy them securely.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely within your WSL 2 environment, so there are no ongoing cloud costs. Nginx does use a small amount of memory while WSL is active.

Resources you used:

  • Nginx web server (installed and running as a service in WSL).
  • Development packages installed via apt (git, curl, tree, jq, htop).
  • Project directory ~/ansible-lab containing all playbooks, inventory, and roles.
  • Ansible virtual environment ~/ansible-env.
  • Deployed files at /var/www/mysite and /etc/myapp.

Keep everything running

No action needed. Choose this if you are still actively building or want to continue experimenting with Ansible.

  • Nginx will continue running inside WSL but consumes minimal resources. It only runs when WSL itself is active.
  • Your playbooks in ~/ansible-lab are ready to re-run or modify anytime.
  • The Ansible virtual environment at ~/ansible-env stays available for future playbook runs.

Pause - I'll come back to this later

Shut down running services to free up memory while keeping your files intact for later.

  • Stop the Nginx service by running:
  • Your playbooks and virtual environment remain on disk, ready for when you come back.
sudo systemctl stop nginx
  • To resume later, activate your virtual environment and re-run your playbook:
source ~/ansible-env/bin/activate
cd ~/ansible-lab
ansible-playbook -i inventory.ini webserver.yml

Delete - I don't want to use this again

Remove all project resources and start fresh. This removes installed packages, project files, deployed content, and the virtual environment.

  • Stop Nginx and remove installed packages by running:
sudo systemctl stop nginx
sudo apt remove nginx git curl tree jq htop -y
sudo apt autoremove -y
  • Remove the deployed files created by your playbooks:
sudo rm -rf /var/www/mysite
sudo rm -rf /etc/myapp
sudo rm -f /etc/nginx/sites-available/mysite
sudo rm -f /etc/nginx/sites-enabled/mysite
  • Deactivate the virtual environment (if active) and remove it:
deactivate
rm -rf ~/ansible-env
  • Remove the project directory:
rm -rf ~/ansible-lab
  • Confirm everything is gone by running:
ls ~/ansible-lab ~/ansible-env 2>&1

You should see "No such file or directory" for both paths.

That's a Wrap!

That's a Wrap!

Nice work! You just automated your development environment like a DevOps engineer.

You've learned how to:

  • Built a complete Ansible project with an inventory, playbooks, variables, handlers, and roles.
  • Proved idempotency by running playbooks twice with zero changes on the second run.
  • Organized automation into reusable roles using ansible-galaxy init.
  • Encrypted secrets with Ansible Vault for secure configuration management.

Ready to quiz yourself?