Ansible for beginners

Ansible for beginners

No automation experience needed. Learn to manage Linux servers with plain YAML files, and practise on three real servers running on your own Windows laptop. Work through it at your own pace in about two hours, or use it to teach a class.

$ ansible-playbook learn_ansible.yml
PLAY [Ansible for beginners] **************************************
TASK [What is Ansible?] ******************************************
ok: [you]
TASK [Compare with Chef, Puppet and SaltStack] *******************
ok: [you]
TASK [Understand the architecture and terms] *********************
ok: [you]
TASK [Write and run playbooks] ***********************************
changed: [you]
TASK [Log in with SSH keys and protect secrets] ******************
ok: [you]
TASK [Run modules and follow best practices] *********************
ok: [you]
PLAY RECAP *******************************************************
you : ok=6 changed=1 unreachable=0 failed=0
About 2 hours self-paced or in a class Free tools only WSL2 and Docker, no cloud account 8 playbooks to run, change and break safely

TASK[Set up your lab]

5 minutes if you did the setup appendix already

You don't need Linux experience or a cloud account. Everything runs on your laptop. Ansible lives inside WSL2, the Linux environment built into Windows, and it manages three small Ubuntu servers running in Docker Desktop.

Check your lab
$ cd ~/ansible-lab
$ docker compose up -d
$ ansible all -m ping -k      # SSH password: ansible

Three green SUCCESS lines mean you are ready. Haven't set up yet, or not working? Jump to the setup appendix, or use the backup container: docker compose exec control bash.

Hands-up check: who gets three green pings? Send anyone stuck to the backup control container so they can follow along. Common causes: Docker Desktop not running, working from /mnt/c so ansible.cfg is ignored, or sshpass missing.

TASK[What is Ansible?]

12 minutes

One server is easy. A hundred is not.

By hand

$ ssh server-01
$ sudo apt install nginx
$ sudo vi /etc/nginx/nginx.conf
$ sudo systemctl restart nginx
# ...and again for server-02 to server-100

Slow, easy to get wrong, and every server drifts a little. Nobody has a record of what was changed.

With Ansible

- name: Web servers
  hosts: webservers    # 1 or 1000 hosts
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx

Describe the result once and apply it everywhere. The file lives in Git, so changes are reviewed and repeatable.

Definition

Ansible is an open-source, agentless automation tool. It configures systems, deploys software and orchestrates IT work using readable YAML files, connecting over SSH to Linux and over WinRM or SSH to Windows.

  • Agentless: nothing to install on the servers you manage, only SSH and Python.
  • YAML: playbooks read like a checklist.
  • History: created by Michael DeHaan in 2012; Red Hat bought it in 2015.
  • Names you will hear: ansible-core is the engine with the builtin modules. The ansible package adds curated community collections (that is what we installed). Red Hat Ansible Automation Platform, and its open-source upstream AWX, add a web UI for teams.

Four ideas behind Ansible

Infrastructure as Code
Servers are described in text files kept in Git, so every change is reviewed, versioned and repeatable.
Configuration management
Keep many machines in a known, consistent state: packages, files, users and services.
Idempotency
Running the same playbook twice gives the same result. If nothing has drifted, the second run changes nothing.
Declarative
You say what you want (β€œnginx is present and running”). The module works out how to get there.

What people automate with it

Configuration management, application deployment and rolling updates, provisioning cloud resources on AWS, Azure, GCP or VMware, orchestrating multi-tier changes in order (database, then app, then load balancer), security patching and compliance, and network devices from Cisco, Juniper and Arista.

Ask how long 100 manual installs would take, and what happens if server 57 gets a typo. Stress the three words: open-source, agentless, YAML. Idempotency is the idea we will prove in the lab by running the same playbook twice.

TASK[Ansible vs Chef vs Puppet vs SaltStack]

10 minutes

AnsibleChefPuppetSaltStack
ArchitectureAgentless, pushAgent, pullAgent, pullAgent or SSH, push and pull
LanguageYAMLRuby DSLPuppet DSLYAML with Jinja
TransportSSH / WinRMHTTPS to Chef ServerHTTPS to Puppet ServerZeroMQ event bus
Setup effortVery lowHighHighMedium
Learning curveEasySteepModerate to steepModerate
Execution orderTop to bottomTop to bottomModel-based, by dependenciesTop to bottom
Speed at scaleGood (parallel forks)GoodGoodVery fast (event-driven)
OwnerRed Hat (IBM)ProgressPerforceBroadcom (VMware)

Push vs pull

No tool is simply β€œbest”. Chef and Puppet are mature and strong at continuous drift correction; Salt is very fast at large scale. Ansible wins on simplicity because there are no agents or central server to run. It can also work in pull style with ansible-pull or scheduled runs in AWX.

Read the Architecture row first; that is the big difference. Ownership changes are context only: Chef to Progress (2020), Puppet to Perforce (2022), Salt to VMware (2020) and then Broadcom (2023).

TASK[Why Ansible?]

5 minutes

Simple
YAML reads like a checklist. Most people are productive on day one.
Agentless
Only SSH and Python on targets. Nothing extra to patch, monitor or secure.
Idempotent
Safe to re-run; only what is different gets changed.
Batteries included
Thousands of modules and collections on Ansible Galaxy.
Works everywhere
Linux, Windows, cloud, containers and network gear.
Big community
A common DevOps skill in job listings, backed by Red Hat.

Honest limits: SSH-based runs across thousands of hosts are slower than agent or event-bus tools, and there is no built-in continuous drift correction unless you schedule runs.

TASK[Architecture]

12 minutes

The control node reads the inventory (which hosts), the playbook (what to do), and uses modules (small programs that do the work) and plugins (connection, output, lookups, filters). Nothing Ansible-specific runs on the managed nodes: no agent, no database, no daemon.

What happens when a task runs

  1. ReadParse ansible.cfg, the inventory, the playbook and variables.
  2. ConnectOpen SSH to each host, several at a time (forks, default 5; the lab uses 10).
  3. CopySend the module, a small Python program, to a temporary folder on the host.
  4. ExecuteRun it. The module compares the current state with the desired state.
  5. ReturnThe module replies with JSON: ok, changed or failed.
  6. Clean upRemove the temporary files and move on to the next task.
See it for yourself
$ ansible-inventory --graph
$ ansible-config dump --only-changed
$ ansible web1 -m ping -vvv      # watch connect, copy, execute, return
This is why Python is needed on Linux targets: modules are Python programs copied over and run. pipelining = True in the lab config skips the copy step, which speeds things up.

TASK[Terminologies]

10 minutes

The building blocks

Control node
The machine Ansible is installed on and run from. Linux or macOS; on Windows, use WSL.
Managed node (host)
Any machine Ansible configures. Needs SSH and Python (Linux) or WinRM (Windows).
Inventory
The list of managed nodes, organised into groups. INI or YAML, static or dynamic.
Module
A unit of work Ansible sends to a host, such as apt, copy, service or user.
Task
One call to a module with arguments, plus a descriptive name.
Play
Maps a group of hosts to a list of tasks.
Playbook
A YAML file containing one or more plays.

The power features

Variables
Values that differ per host, group or environment, such as http_port: 80.
Facts
Information Ansible discovers about each host (OS, IP, memory) using the setup module.
Handlers
Tasks that run only when another task notifies them of a change, like restarting nginx.
Templates
Jinja2 files (.j2) filled in with variables to produce real config files.
Roles
A standard folder layout that packages tasks, handlers, templates and variables for reuse.
Collections
The distribution format for modules, roles and plugins, such as ansible.posix.
Ansible Galaxy
The public hub for sharing roles and collections.
Vault
Built-in encryption for passwords, keys and other secrets.

Inventory: telling Ansible which hosts exist

[webservers]
web1 ansible_host=127.0.0.1 ansible_port=2221
web2 ansible_host=127.0.0.1 ansible_port=2222

[dbservers]
db1  ansible_host=127.0.0.1 ansible_port=2223

[lab:children]     # a group of groups
webservers
dbservers
all:
  children:
    lab:
      children:
        webservers:
          hosts:
            web1:
            web2:
        dbservers:
          hosts:
            db1:

Every inventory has two built-in groups, all and ungrouped. You target hosts with patterns such as web1, webservers, webservers:dbservers or all:!db1. Dynamic inventory plugins can read hosts straight from AWS, Azure or VMware.

Where variables live

  • -e "x=1" extra varsalways wins
  • task and block vars
  • play vars, vars_files
  • host_vars/db1.ymlone host
  • group_vars/webservers.ymlone group
  • group_vars/all.ymlevery host
  • role defaults/main.ymleasiest to override
# inventory/group_vars/webservers.yml
http_port: 80
page_title: "Hello from Ansible"

# inventory/host_vars/db1.yml
server_role: database

# use it anywhere with Jinja2
msg: "Port is {{ http_port }}"

The more specific location wins. Official precedence has 22 levels, but this simplified ladder covers day-to-day work.

Quick check: β€œIs apt a task or a module?” (a module). Live: ansible web1 -m setup -a "filter=ansible_distribution*" to show facts.

PAUSE[Take a short break]

Halfway there. Before you continue, run docker ps and ansible all -m ping.

TASK[Playbook structure]

20 minutes, including hands-on

YAML in five minutes

---                    # optional document start
name: Alice            # key: value (a mapping)
is_admin: true         # booleans: true / false
packages:              # a list
  - nginx
  - curl
user:                  # a nested mapping
  name: bob
  shell: /bin/bash
inline_list: [a, b, c]
message: "Quote values that contain {{ vars }} or colons"
script: |              # keep line breaks
  line one
  line two
  • Spaces only, never tabs.
  • Indent consistently, two spaces is the norm.
  • - starts a list item.
  • Put a space after the colon: key: value.
  • Quote any value that starts with {{.
  • Check your work with --syntax-check or ansible-lint.

Playbook, play, task, module

Playbook
A YAML file holding an ordered list of plays.
Play
Targets a group of hosts and sets options such as become, vars and gather_facts.
Task
One step. It runs on every host in the play before the next task starts.
Module
The code that does the work for a task.

Anatomy of a playbook

The web server playbook from the labplaybooks/03_webserver.yml
---
- name: Install and configure nginx    # play name
  hosts: webservers                    # which inventory group
  become: true                         # run as root via sudo
  vars:
    owner_team: "Ansible learners"

  tasks:
    - name: Install nginx
      ansible.builtin.apt:              # module, written with its full name
        name: nginx                     # module arguments describe the state
        state: present
        update_cache: true

    - name: Deploy the home page from a template
      ansible.builtin.template:
        src: templates/index.html.j2
        dest: /var/www/html/index.html
        mode: "0644"
      notify: Restart nginx            # triggers the handler below

    - name: Ensure nginx is running
      ansible.builtin.service:
        name: nginx
        state: started

  handlers:
    - name: Restart nginx               # runs once, at the end, only if notified
      ansible.builtin.service:
        name: nginx
        state: restarted
Try it: run it, then run it again
$ ansible-playbook playbooks/03_webserver.yml --check    # dry run
$ ansible-playbook playbooks/03_webserver.yml            # for real
$ ansible-playbook playbooks/03_webserver.yml            # again: changed=0

Open localhost:8081 and localhost:8082 in your browser. Then change page_title in inventory/group_vars/webservers.yml and run with --diff: only the template changes, so the handler fires.

Loops, conditionals and register

- name: Install packages                    # loop: repeat for each item
  ansible.builtin.apt:
    name: "{{ item }}"
  loop: [curl, tree, vim-tiny]

- name: Only on Debian-family systems       # when: run only if true
  ansible.builtin.debug:
    msg: "apt is the right module here"
  when: ansible_facts['os_family'] == "Debian"

- name: Read uptime                         # register: save the result
  ansible.builtin.command: uptime
  register: up
  changed_when: false                       # reading never changes the server

- name: Show it
  ansible.builtin.debug:
    var: up.stdout

when takes a Jinja2 expression without the curly braces. changed_when: false keeps the report honest for commands that only read data.

Roles: organise playbooks for reuse

ansible-lab/
β”œβ”€β”€ ansible.cfg
β”œβ”€β”€ inventory/
β”‚   β”œβ”€β”€ wsl.ini
β”‚   β”œβ”€β”€ group_vars/
β”‚   └── host_vars/
β”œβ”€β”€ playbooks/site.yml
└── roles/
    └── nginx/
        β”œβ”€β”€ tasks/main.yml
        β”œβ”€β”€ handlers/main.yml
        β”œβ”€β”€ templates/index.html.j2
        β”œβ”€β”€ files/
        β”œβ”€β”€ vars/main.yml
        β”œβ”€β”€ defaults/main.yml
        └── meta/main.yml
tasks/
The entry point, main.yml.
handlers/
Restart and reload handlers.
templates/
Jinja2 .j2 files.
files/
Static files for copy.
defaults/
Lowest-priority variables.
meta/
Author and dependencies.
- hosts: webservers
  become: true
  roles:
    - nginx

Create a skeleton with ansible-galaxy role init roles/myrole. Try ansible-playbook playbooks/site.yml --tags motd to run only part of a role.

Reading the output

okAlready in the desired state; nothing done.
changedAnsible made a change.
failedThe task hit an error; this host stops.
skippedA when condition was false.
unreachableCould not connect: SSH or login problem.
PLAY RECAP *********************************************
web1 : ok=5  changed=0  unreachable=0  failed=0  skipped=0
web2 : ok=6  changed=3  unreachable=0  failed=0  skipped=0

Useful flags: --syntax-check, --check, --diff, --limit web1, --tags motd, --list-tasks and -v up to -vvvv.

Most beginner errors are YAML indentation; recommend the Red Hat YAML and Ansible extensions for VS Code. By default a task runs on all hosts before the next task starts (the linear strategy); a failed host drops out, the rest continue. The lab playbook also has a lineinfile task that removes nginx's IPv6 listener, because some Docker setups have no IPv6.

TASK[Authentication mechanism]

10 minutes, including hands-on

Authentication is really three separate questions.

1. How do I log in?
SSH password (-k, needs sshpass) or, preferably, an SSH key pair. Windows uses WinRM (NTLM, Kerberos or certificates) or SSH. ansible_user or remote_user sets the login name.
2. How do I get root?
Privilege escalation with become: true (sudo by default), become_user to switch to another account, and -K to prompt for the sudo password.
3. How do I protect secrets?
Ansible Vault encrypts files with AES-256. no_log: true hides values in output. Larger teams often use HashiCorp Vault or CyberArk.

From password to SSH keys

  1. Create a key pair on the control nodessh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
  2. First contact with a passwordansible all -m ping -k and type ansible
  3. Copy the public key to every nodeansible-playbook playbooks/06_ssh_keys.yml -k
  4. Log in without a password from now onansible all -m ping
The playbook that copies the keyplaybooks/06_ssh_keys.yml
- name: Distribute the control node's public key
  hosts: all
  gather_facts: false
  tasks:
    - name: Add public key to ~ansible/.ssh/authorized_keys
      ansible.posix.authorized_key:
        user: ansible
        state: present
        key: "{{ lookup('ansible.builtin.file', lookup('ansible.builtin.env', 'HOME') + '/.ssh/id_ed25519.pub') }}"

The private key never leaves the control node. Only the public key is copied to the servers.

Privilege escalation

- hosts: dbservers
  become: true                       # the whole play runs as root
  tasks:
    - name: Create a database as the postgres user
      ansible.builtin.command: createdb app
      become_user: postgres          # just this task

Only escalate where you need to. Ad-hoc equivalents are -b to become root and -K to be asked for the sudo password.

Ansible Vault

$ cp vault/secrets.example.yml vault/secrets.yml
$ ansible-vault encrypt vault/secrets.yml       # choose a vault password
$ cat vault/secrets.yml                          # $ANSIBLE_VAULT;1.1;AES256 ...
$ ansible-vault view vault/secrets.yml
$ ansible-vault edit vault/secrets.yml
$ ansible-playbook playbooks/07_vault.yml --ask-vault-pass

Never commit plain-text passwords. Commit the encrypted file and share the vault password through a separate channel. The lab's .gitignore already excludes vault/secrets.yml.

Do the key switch live. The lab turns off host key checking and known_hosts only because containers are rebuilt often; keep host key checking on in real environments. In production: SSH keys, a dedicated automation user with limited sudo, and secrets in a vault.

TASK[Executing Ansible modules]

10 minutes, hands-on

An ad-hoc command runs a single module from the terminal, with no playbook.

ansiblethe command
webservershost pattern
-bbecome root
-m aptmodule
-a "name=tree state=present"module arguments

Use ad-hoc for

  • Quick checks: ping, uptime, disk space
  • One-off fixes, like restarting a service
  • Gathering facts from many hosts
  • Trying a module before writing a task

Use a playbook for

  • Anything repeatable or multi-step
  • Changes that should be reviewed in Git
  • Handlers, templates and roles
  • Documenting how a server is built
Try these now
$ ansible all -m ansible.builtin.command -a "hostname"
$ ansible all -m ansible.builtin.shell -a "df -h | grep /"
$ ansible web1 -m ansible.builtin.setup -a "filter=ansible_distribution*"
$ ansible webservers -b -m ansible.builtin.apt -a "name=tree state=present update_cache=true"
$ ansible all -b -m ansible.builtin.copy -a "content='hello' dest=/tmp/hello.txt"
$ ansible dbservers -b -m ansible.builtin.user -a "name=carol state=present"
$ ansible webservers -b -m ansible.builtin.service -a "name=nginx state=restarted"
$ ansible 'webservers:!web2' -m ping       # all web servers except web2

Run the apt command twice. The first run reports changed, the second ok. That is idempotency. More commands are in CHEATSHEET.md.

command, shell or raw?

ModuleWhat it doesGood forWatch out
commandRuns a program directly, no shell. The default module.Simple commands; safe from shell injectionNo pipes, redirects, $VARS or &&
shellRuns through /bin/sh on the node.Pipes, redirects, environment variablesEasy to write tasks that are not idempotent
rawSends a plain SSH command; needs no Python.Installing Python on a new host; network devicesNo state checking at all

Prefer a dedicated module (apt, copy, service) over all three. If you must run a command, prefer command, and add creates, removes or changed_when so the task reports honestly.

Watch for people forgetting -b on package installs (permission errors) and for quoting mistakes in -a. The default module is command, so ansible all -a uptime also works.

TASK[Modules]

8 minutes

Packages
apt, dnf, yum, package, pip
Files
copy, file, template, lineinfile, fetch
Services
service, systemd, cron
Users and security
user, group, authorized_key, firewalld
Commands
command, shell, script, raw
Source and network
git, get_url, uri, unarchive
Cloud
amazon.aws, azure.azcollection, google.cloud collections
Windows
win_package, win_service, win_copy

The ten you will use most

ModuleWhat it doesExample arguments
pingTests the connection and Pythonansible all -m ping
apt / dnfInstalls or removes packagesname=nginx state=present
copyCopies a file or writes contentsrc=app.conf dest=/etc/app.conf
templateRenders a Jinja2 templatesrc=index.html.j2 dest=/var/www/html/index.html
fileCreates folders and links; sets permissionspath=/opt/app state=directory mode=0755
lineinfileMakes sure one line is in a filepath=/etc/hosts line="10.0.0.5 db1"
serviceStarts, stops, restarts and enables servicesname=nginx state=started enabled=true
user / groupManages accountsname=alice groups=trainees append=true
command / shellRuns commands, as a last resortcmd: uptime
debugPrints messages and variablesvar: result.stdout

Look modules up instead of memorising them

$ ansible-doc -l | grep -i apt            # list and search modules
$ ansible-doc ansible.builtin.copy        # full documentation with examples
$ ansible-doc -s ansible.builtin.user     # short snippet to paste into a task
$ ansible-galaxy collection list          # installed collections
$ ansible-galaxy collection install community.general

Always write the fully qualified collection name (FQCN): ansible.builtin.copy rather than copy. It reads as namespace ansible, collection builtin, module copy. It avoids clashes between collections, and ansible-lint flags short names. Full docs are at docs.ansible.com.

The examples section at the bottom of every ansible-doc page is the most useful part for beginners. For every module, state is the key argument: present or absent, started, stopped or restarted, directory, file or absent.

TASK[Best practices]

8 minutes

Name every task
Clear names make output and failures easy to read.
Modules over shell
Dedicated modules are idempotent; shell commands rarely are.
Use FQCN
ansible.builtin.copy, never a bare copy.
Structure with roles
Small roles with one job each, reused across playbooks.
Separate data from code
Values in group_vars and host_vars; logic in tasks.
Keep secrets in Vault
Never commit plain-text passwords or keys.
Check before you change
--syntax-check, --check --diff and ansible-lint.
Least privilege
Use become only on tasks that need root.
Version everything
Git, code review, and CI that runs ansible-lint.

Same goal, better task

Avoid

- shell: apt-get install -y nginx

- shell: echo "ServerName x" >> /etc/app.conf

- copy: src=db.conf dest=/etc/
  # the password is inside db.conf, in Git

- hosts: all
  become: true      # every task as root

Prefer

- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present

- name: Set ServerName once
  ansible.builtin.lineinfile:
    path: /etc/app.conf
    line: "ServerName x"
# secrets in Vault, become per task

The echo >> version appends a new line every time it runs. Run it five times and you get five lines. The lineinfile version is idempotent.

Lint the lab
$ ansible-lint playbooks roles

The lab project passes the strictest production profile, so it is a good reference for your own work.

TASK[Recap and quiz]

5 minutes

Ansible
Agentless, YAML-based, push-style automation over SSH or WinRM.
Versus others
Chef and Puppet use agents and pull; Salt uses an event bus.
Architecture
Control node with inventory, playbooks and modules, connecting to managed nodes.
Playbooks
Playbook, plays, tasks, modules; plus handlers, variables and roles.
Authentication
SSH keys to connect, become for root, Vault for secrets.
Modules
Ad-hoc for one-offs; dedicated modules over shell; look them up with ansible-doc.

Quick quiz

What makes Ansible agentless?

It connects over SSH or WinRM, copies small modules, runs them and cleans up. Nothing stays running on the managed node.

What does idempotent mean?

Running the same task again produces the same end state, and reports no change if nothing drifted.

What is the difference between a play and a task?

A play maps a group of hosts to a list of tasks. A task is a single call to one module.

When does a handler run?

Once, at the end of the play, and only if a task that notifies it reported changed.

Which module adds one line to a file safely?

ansible.builtin.lineinfile.

Why prefer SSH keys over passwords?

No password is sent or stored, logins can be automated safely, and keys can be revoked per machine.

Keep going after today

  • Finish all seven exercises in EXERCISES.md, plus the stretch goals.
  • Read the getting-started and playbook guides at docs.ansible.com.
  • Browse roles and collections on galaxy.ansible.com.
  • Automate one real chore: set up your own development machine with a playbook.

Stop the lab when you are done with docker compose stop, and start it again any time with docker compose start.

Good follow-up session topics: Jinja2 templates in depth, error handling with block and rescue, dynamic inventory for cloud, AWX and Automation Platform, and Molecule for testing roles.

TASK[Appendix: set up the lab on Windows]

Do this first, about 40 minutes the first time. Full troubleshooting is in the README.

You need Windows 10 22H2 or Windows 11, 8 GB of RAM (16 GB is comfortable), 15 GB of free disk, virtualization turned on in the BIOS, admin rights and internet access.

  1. Check the prerequisitesIn PowerShell, from the lab folder: powershell -ExecutionPolicy Bypass -File .\scripts\check-prereqs.ps1
  2. Install WSL2 with UbuntuPowerShell as Administrator: wsl --install -d Ubuntu-24.04, reboot, then create your Linux user.
  3. Install Docker DesktopKeep the WSL 2 backend. In Settings, Resources, WSL integration, turn on Ubuntu-24.04. Test in Ubuntu with docker run --rm hello-world.
  4. Install VS Code (recommended)Add the WSL, Ansible and YAML extensions, then run code . from the lab folder.
  5. Get the lab into your Linux home foldergit clone https://github.com/YOUR-USER/ansible-lab.git ~/ansible-lab
    Do not work from /mnt/c/…: Windows folders look world-writable to Linux, so Ansible ignores ansible.cfg there.
  6. Install Ansiblecd ~/ansible-lab && bash scripts/setup-wsl.sh && source ~/.bashrc
  7. Start the serversdocker compose up -d --build, then docker ps should list web1, web2, db1 and control.
  8. Smoke testansible all -m ping -k with the password ansible. Three green lines and you are done.

Can't install WSL? Run docker compose up -d --build in PowerShell, then docker compose exec control bash. That container already has Ansible and works from /ansible.

Common problems

What you seeFix
ansible: command not foundRun source ~/.bashrc or open a new terminal.
β€œworld writable directory … ignoring ansible.cfg”You are in /mnt/c. Move the lab to ~.
β€œyou must install the sshpass program”sudo apt install -y sshpass
UNREACHABLE … Connection refusedThe containers are not running: docker compose up -d.
docker: command not found in WSLTurn on WSL integration for Ubuntu in Docker Desktop settings.
Permission denied (publickey,password)Use -k with password ansible, or rerun 06_ssh_keys.yml -k after a lab reset.
Downloads fail on the office networkSet the proxy in Docker Desktop, and export https_proxy=http://proxy:port in WSL.