Installation and Setup
Before we start, let’s install Ansible and set up our lab. Ansible gets installed only on the control node — the machine you run Ansible from — while the managed nodes just need to be reachable over SSH. The control node can also act as a managed node, but it’s better to use separate hosts here: it gives you a much clearer picture of how Ansible actually connects to and operates on remote machines, rather than blurring that line from day one.
Ansible Installation
Follow the official installation guide for whichever method suits you. If you’re following along with me, I’m using pip inside a Python virtual environment. Make sure python3 and pip are available on your control node first — and that the control node itself is some flavor of Linux, or at minimum WSL if you’re on Windows.
You don’t need to know Python to learn Ansible, but knowing the language definitely shapes your journey for the better down the line. Either way, here’s what you need:
# Step 1: Create a dedicated ansible directory
mkdir ansible && cd ansible
# Step 2: Create a Python virtual environment
python3 -m venv .venv
# Step 3: Activate the virtual environment
source .venv/bin/activate
# Step 4: Install ansible
pip3 install ansibleNote
On Debian and Ubuntu-based systems, python3 -m venv sometimes fails with something like ensurepip is not available — the venv module isn’t bundled with the base python3 package on these distributions. If that happens, install it separately first: sudo apt install python3-venv.
Note
If you’re new to Python, I’d recommend installing Ansible system-wide instead — skip Step 2 and Step 3. Otherwise, you’ll need to reactivate the virtual environment every time you log in or start a new shell session before any Ansible command is available.
Once installed, confirm it with:
ansible --versionThis prints the installed Ansible version, along with the Python interpreter and config file it’s using — worth a quick glance now, since you’ll come back to that config file path in a later chapter.
Tip
pip install ansible pulls in the full community package — Ansible itself plus a large collection of bundled modules and plugins for third-party tools and cloud providers. There’s a smaller alternative, ansible-core, with just the engine and no bundled collections. Unless you already know you want the minimal footprint, the full ansible package is the right default while you’re learning.
Managed Nodes Setup
You’ll need at least one extra virtual or remote machine to act as a managed node. This can be a VirtualBox VM, a cloud instance, a spare machine on your network, a container — whatever you have available. If you’re comfortable with Docker, you can follow my own lab setup below, built entirely from containers.
Dockerfile For Host1 (Ubuntu)
FROM ubuntu
# install ssh server, sudo, and python3 (required by most Ansible modules)
RUN <<EOF
apt-get update
apt-get install -y sudo openssh-server python3
EOF
# enable pubkey auth only
RUN <<EOF
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config
EOF
# user setup
RUN <<EOF
useradd -m -s /bin/bash alice
echo "alice:iamalice" | chpasswd
useradd -m -s /bin/bash bob
echo "bob:iambob" | chpasswd
echo 'alice ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
echo 'bob ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
usermod -aG sudo alice
usermod -aG sudo bob
EOF
# setup alice's key
COPY --chmod=644 --chown=alice:alice alice.pub /home/alice/.ssh/authorized_keys
RUN chown -R alice:alice /home/alice/.ssh && chmod 700 /home/alice/.ssh
# setup bob's key
COPY --chmod=644 --chown=bob:bob bob.pub /home/bob/.ssh/authorized_keys
RUN chown -R bob:bob /home/bob/.ssh && chmod 700 /home/bob/.ssh
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]Note
The official ubuntu image is intentionally minimal and doesn’t include Python by default. Ansible needs a Python interpreter on the managed node for the vast majority of its modules to work — without it, even a simple ad hoc command in the next chapter would fail. That’s why python3 is installed explicitly above, alongside sudo and openssh-server.
Dockerfile For Host2 (Fedora)
FROM fedora
# server setup
RUN dnf install openssh-server sudo python3 clear -y
RUN ssh-keygen -A
# enable pubkey auth only
RUN <<EOF
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config
EOF
# user setup
RUN <<EOF
useradd -m -s /bin/bash alice
echo "alice:iamalice" | chpasswd
useradd -m -s /bin/bash bob
echo "bob:iambob" | chpasswd
echo 'alice ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
echo 'bob ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
usermod -aG wheel alice
usermod -aG wheel bob
EOF
# setup alice's key
COPY --chmod=644 --chown=alice:alice alice.pub /home/alice/.ssh/authorized_keys
RUN chown -R alice:alice /home/alice/.ssh && chmod 700 /home/alice/.ssh
# setup bob's key
COPY --chmod=644 --chown=bob:bob bob.pub /home/bob/.ssh/authorized_keys
RUN chown -R bob:bob /home/bob/.ssh && chmod 700 /home/bob/.ssh
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]Warning
Inside a RUN <<EOF ... EOF block, every line runs as a plain shell command — not as a separate Dockerfile instruction. Writing RUN usermod ... inside the heredoc (rather than just usermod ...) will fail the build, since the shell will try to run a literal command named RUN, which doesn’t exist. Keep heredoc contents as pure shell.
Keys For Containers
For user alice:
ssh-keygen -t ed25519 -f alice -N "" -qFor user bob:
ssh-keygen -t ed25519 -f bob -N "" -qChange permissions on the private keys so SSH doesn’t complain about them being too open:
chmod 400 alice bobThe docker-compose File
name: ansible
services:
ubuntu:
image: myubuntu
build:
dockerfile: Dockerfile.ubuntu
networks:
demonet:
ipv4_address: 10.0.0.1
fedora:
image: myfedora
build:
dockerfile: Dockerfile.fedora
networks:
demonet:
ipv4_address: 10.0.0.2
networks:
demonet:
ipam:
driver: default
config:
- subnet: 10.0.0.0/24
gateway: 10.0.0.254One Command: You Are Live
That’s the beauty of docker compose — one command, and the whole setup is ready:
docker compose up -dVerify Your Setup
Before bringing Ansible into the picture at all, confirm you can actually reach both containers over plain SSH. If this doesn’t work, nothing in the next chapter will either — and it’s much easier to debug a raw SSH connection now than to guess whether a later “unreachable” error is your lab or Ansible itself.
ssh -i alice [email protected]ssh -i bob [email protected]Note
The first time you connect to each host, SSH will ask whether you want to continue connecting, since it doesn’t yet recognize the host’s key. Type yes and press Enter — this is expected, and only happens once per host.
If both connections drop you into a shell prompt, type exit to leave each session — your lab is ready. If either one fails, double-check the container is actually running (docker compose ps) and that the key file permissions from the previous step were applied correctly.
Remember Even If You Don’t Follow This Setup
Even if you set up your managed nodes differently, keep these in mind going forward:
aliceandbobare both in thesudo(Ubuntu) orwheel(Fedora) group — they can perform privileged actions.- The
ubuntucontainer’s IP address is10.0.0.1, and thefedoracontainer’s is10.0.0.2. - Every managed node needs a working Python interpreter — most Ansible modules depend on it, and it’s an easy thing to forget on a minimal base image.
When you’re done experimenting and want to tear the lab down, docker compose down removes the containers and network cleanly, ready to be rebuilt with docker compose up -d whenever you come back to it.