Skip to main content
🎤 Luca Berton is speaking at Red Hat Summit & KubeCon EU 2026!Learn more →
Back to Blog

Ansible Exercises for Beginners

Practice Ansible with hands-on exercises. From basic playbooks to roles and Vault, build real automation skills step by step.

Luca BertonApril 5, 20262 min read

Reading about Ansible is not enough — you need to practice. These exercises progress from basic to intermediate, each building on the last.

Setup

You need two machines: a control node (your laptop) and at least one managed node (VM or container).

Quick lab with Docker:

bash
# Start a target container with SSH
docker run -d --name target \
  -p 2222:22 \
  rastasheep/ubuntu-sshd:18.04

# Test SSH access
ssh root@localhost -p 2222
# Password: root

Create inventory.ini:

ini
[lab]
localhost ansible_port=2222 ansible_user=root ansible_password=root ansible_ssh_common_args='-o StrictHostKeyChecking=no'

Test connectivity:

bash
ansible -i inventory.ini lab -m ping

Exercise 1: Ad-hoc Commands

Goal: Run commands on remote hosts without a playbook.

bash
# Check uptime
ansible -i inventory.ini lab -m command -a "uptime"

# Check disk space
ansible -i inventory.ini lab -m command -a "df -h"

# Install a package
ansible -i inventory.ini lab -m apt -a "name=curl state=present" --become

# Create a file
ansible -i inventory.ini lab -m file -a "path=/tmp/hello state=touch"

What you learn: Ansible modules work without playbooks too.

Exercise 2: First Playbook

Goal: Write a playbook that installs and configures a web server.

Create exercise2.yml:

yaml
---
- name: Setup web server
  hosts: lab
  become: true
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
        update_cache: true

    - name: Create custom index page
      copy:
        content: |
          <html>
          <body>
          <h1>Hello from Ansible!</h1>
          <p>Server: {{ inventory_hostname }}</p>
          </body>
          </html>
        dest: /var/www/html/index.html

    - name: Ensure Nginx is running
      service:
        name: nginx
        state: started
        enabled: true

Run it:

bash
ansible-playbook -i inventory.ini exercise2.yml

Challenge: Run it twice. Notice how the output shows "changed" vs "ok" — that is idempotency.

Related Course

Master this topic with hands-on labs

Go beyond reading — build real projects in sandboxed environments with expert video guidance.

Browse Courses →

Exercise 3: Variables and Facts

Goal: Use variables and gather system information.

Create exercise3.yml:

yaml
---
- name: Variables and facts
  hosts: lab
  become: true
  vars:
    app_name: "myapp"
    packages:
      - htop
      - vim
      - tree

  tasks:
    - name: Display OS information
      debug:
        msg: "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"

    - name: Display memory info
      debug:
        msg: "RAM: {{ ansible_memtotal_mb }} MB"

    - name: Install packages from list
      apt:
        name: "{{ packages }}"
        state: present
        update_cache: true

    - name: Create app directory
      file:
        path: "/opt/{{ app_name }}"
        state: directory
        mode: "0755"

Challenge: Add a variable for the directory mode and make it configurable.

Exercise 4: Conditionals and Loops

Goal: Control task execution with when and iterate with loop.

Create exercise4.yml:

yaml
---
- name: Conditionals and loops
  hosts: lab
  become: true
  vars:
    users:
      - { name: alice, shell: /bin/bash, admin: true }
      - { name: bob, shell: /bin/bash, admin: false }
      - { name: charlie, shell: /bin/sh, admin: false }

  tasks:
    - name: Create users
      user:
        name: "{{ item.name }}"
        shell: "{{ item.shell }}"
        state: present
      loop: "{{ users }}"

    - name: Add admin users to sudo group
      user:
        name: "{{ item.name }}"
        groups: sudo
        append: true
      loop: "{{ users }}"
      when: item.admin

    - name: Show created admin users
      debug:
        msg: "{{ item.name }} is an admin"
      loop: "{{ users }}"
      when: item.admin

Challenge: Add a state field to each user and handle present/absent.

Exercise 5: Templates (Jinja2)

Goal: Generate configuration files dynamically.

Create templates/app.conf.j2:

jinja2
# Application configuration
# Generated by Ansible on {{ ansible_date_time.iso8601 }}

APP_NAME={{ app_name }}
APP_PORT={{ app_port }}
APP_ENV={{ env }}
DB_HOST={{ db_host }}
DB_PORT={{ db_port }}
WORKERS={{ ansible_processor_vcpus * 2 }}

Create exercise5.yml:

yaml
---
- name: Template exercise
  hosts: lab
  become: true
  vars:
    app_name: "webapp"
    app_port: 8080
    env: "production"
    db_host: "db.internal"
    db_port: 5432

  tasks:
    - name: Deploy app configuration
      template:
        src: templates/app.conf.j2
        dest: /etc/app.conf
        mode: "0644"

    - name: Show generated config
      command: cat /etc/app.conf
      register: config_output

    - name: Display config
      debug:
        var: config_output.stdout_lines

Challenge: Add a list of allowed IPs to the template using a Jinja2 for loop.

Stay Updated

Get weekly IT automation tips

Docker, Ansible, Terraform, MLOps — curated insights delivered to your inbox. No spam.

Subscribe Free →

Exercise 6: Handlers

Goal: Trigger actions only when changes occur.

Create exercise6.yml:

yaml
---
- name: Handler exercise
  hosts: lab
  become: true
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
      notify: Start Nginx

    - name: Update Nginx config
      copy:
        content: |
          server {
              listen 8080;
              location / {
                  return 200 'Hello from port 8080';
              }
          }
        dest: /etc/nginx/sites-available/custom
      notify: Reload Nginx

  handlers:
    - name: Start Nginx
      service:
        name: nginx
        state: started

    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

What you learn: Handlers only run when notified by a changed task — efficient and safe.

Exercise 7: Roles

Goal: Organize tasks into a reusable role.

bash
mkdir -p roles/webserver/{tasks,templates,handlers,defaults}

roles/webserver/defaults/main.yml:

yaml
http_port: 80
server_name: localhost

roles/webserver/tasks/main.yml:

yaml
---
- name: Install Nginx
  apt:
    name: nginx
    state: present

- name: Deploy site config
  template:
    src: site.conf.j2
    dest: /etc/nginx/sites-available/default
  notify: Reload Nginx

roles/webserver/handlers/main.yml:

yaml
---
- name: Reload Nginx
  service:
    name: nginx
    state: reloaded

Use the role:

yaml
---
- name: Apply webserver role
  hosts: lab
  become: true
  roles:
    - webserver

Challenge: Add variables to customize the port and server name.

What's Next?

Our Ansible Automation in 30 Minutes course provides guided exercises in real lab environments — no local VM setup needed. The first lesson is free.

---

Ready to go deeper? Check out our hands-on course: Ansible Quickstart — practical exercises you can follow along on your own machine.

Ready to learn by doing?

Stop reading tutorials — start building. Expert video courses with hands-on labs in real sandboxed environments.

Share this article
LB
Luca Berton

Docker Captain, IT automation expert, Red Hat Summit & KubeCon speaker. Building hands-on education for DevOps engineers at CopyPasteLearn.

Related Articles

Explore topics

Browse more articles on the topics covered here.