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?