En este laboratorio voy a preparar varios sistemas Linux en Docker para utilizarlos posteriormente como nodos administrados mediante Ansible.

La idea es tener un entorno pequeño pero variado:

debian1    Debian 13       172.30.10.11
debian2    Debian 13       172.30.10.12
rocky1     Rocky Linux 9   172.30.10.21
rocky2     Rocky Linux 9   172.30.10.22
alpine1    Alpine 3.24     172.30.10.31
mysql1     MySQL 8.4       172.30.10.40

Todos los nodos estarán conectados a la red Docker ansible-lab, con acceso SSH mediante el usuario ansible.

%%{init: {
  "theme": "base",
  "themeVariables": {
    "fontFamily": "sans-serif",
    "fontSize": "13px",
    "edgeLabelBackground": "#ffffff"
  }
} }%%
flowchart TD
    classDef host fill:#2b3e50,stroke:#1a252f,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef net fill:#34495e,stroke:#2c3e50,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef debian fill:#2980b9,stroke:#1f618d,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef rocky fill:#16a085,stroke:#117a65,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef alpine fill:#d35400,stroke:#ba4a00,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef db fill:#8e44ad,stroke:#7d3c98,stroke-width:2px,color:#fff,rx:8,ry:8;

    M["Manjaro<br/><b>Nodo de control</b>"]:::host

    subgraph LAB ["Red Docker: ansible-lab (172.30.10.0/24)"]
        NET["Docker Bridge<br/>172.30.10.1"]:::net
        
        subgraph NODES ["Contenedores Gestionados"]
            direction LR
            subgraph DEB ["Debian 13"]
                direction TB
                D1["debian1<br/>172.30.10.11"]:::debian
                D2["debian2<br/>172.30.10.12"]:::debian
            end
            subgraph RCK ["Rocky Linux 9"]
                direction TB
                R1["rocky1<br/>172.30.10.21"]:::rocky
                R2["rocky2<br/>172.30.10.22"]:::rocky
            end
            subgraph ALP ["Alpine 3.24"]
                direction TB
                A1["alpine1<br/>172.30.10.31"]:::alpine
            end
            subgraph DAT ["Base de Datos"]
                direction TB
                DB["mysql1<br/>172.30.10.40"]:::db
            end
        end
    end

    M ==> NET
    NET --- D1
    NET --- D2
    NET --- R1
    NET --- R2
    NET --- A1
    NET --- DB

    linkStyle default stroke:#7f8c8d,stroke-width:1.5px;

1. Creo la estructura del proyecto

Empiezo creando las carpetas:

mkdir -p ansible-docker-lab/{ssh,images/{debian,rocky,alpine,mysql}}
cd ansible-docker-lab

La estructura será:

ansible-docker-lab/
├── compose.yml
├── .env
├── ssh/
│   └── ansible_lab.pub
└── images/
    ├── debian/
    │   └── Dockerfile.systemd
    ├── rocky/
    │   └── Dockerfile.systemd
    ├── alpine/
    │   └── Dockerfile
    └── mysql/
        ├── Dockerfile
        └── entrypoint-lab.sh

Creo los archivos:

touch compose.yml
touch .env

touch images/debian/Dockerfile.systemd
touch images/rocky/Dockerfile.systemd
touch images/alpine/Dockerfile
touch images/mysql/Dockerfile
touch images/mysql/entrypoint-lab.sh

2. Creo la clave SSH

Genero una clave específica para el laboratorio:

ssh-keygen -t ed25519 \
  -f ~/.ssh/ansible_lab_ed25519 \
  -C "ansible-docker-lab"

Copio solo la clave pública:

cp ~/.ssh/ansible_lab_ed25519.pub ssh/ansible_lab.pub

La clave privada se queda únicamente en mi equipo.

3. Creo el Dockerfile de Debian

Debian y Rocky van a ejecutar systemd dentro del contenedor. Esto permite gestionar servicios de una forma muy parecida a una máquina Linux normal.

Edito:

vim images/debian/Dockerfile.systemd

y creo una imagen basada en Debian 13.

Primero instalo los componentes principales:

FROM debian:trixie

ENV container=docker

RUN apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y \
        systemd \
        systemd-sysv \
        dbus \
        openssh-server \
        sudo \
        python3 \
        python3-apt \
        procps \
        iproute2 \
        iputils-ping \
        curl \
        vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Después creo el usuario que utilizará Ansible:

RUN useradd --create-home --shell /bin/bash ansible && \
    echo "ansible ALL=(ALL) NOPASSWD:ALL" \
        > /etc/sudoers.d/ansible && \
    chmod 0440 /etc/sudoers.d/ansible

Instalo la clave pública:

RUN mkdir -p /home/ansible/.ssh

COPY ssh/ansible_lab.pub /home/ansible/.ssh/authorized_keys

RUN chown -R ansible:ansible /home/ansible/.ssh && \
    chmod 700 /home/ansible/.ssh && \
    chmod 600 /home/ansible/.ssh/authorized_keys

Configuro SSH para usar autenticación por clave:

RUN printf '%s\n' \
    'PermitRootLogin no' \
    'PasswordAuthentication no' \
    'PubkeyAuthentication yes' \
    > /etc/ssh/sshd_config.d/ansible-lab.conf

Finalmente preparo systemd y dejo que sea el proceso principal:

RUN systemctl enable ssh

RUN systemctl mask systemd-modules-load.service

STOPSIGNAL SIGRTMIN+3

CMD ["/lib/systemd/systemd"]

La idea del Dockerfile es sencilla: parto de Debian, añado SSH, Python y sudo, creo el usuario ansible y convierto el contenedor en un pequeño servidor administrable.

4. Creo el Dockerfile de Rocky Linux

Creo:

vim images/rocky/Dockerfile.systemd

La estructura es prácticamente la misma, pero utilizando las herramientas propias de Rocky Linux.

FROM rockylinux:9

ENV container=docker

RUN dnf -y install \
        systemd \
        openssh-server \
        sudo \
        python3 \
        python3-dnf \
        procps-ng \
        iproute \
        iputils \
        curl-minimal \
        vim-minimal && \
    dnf clean all

Creo el usuario:

RUN useradd --create-home --shell /bin/bash ansible && \
    echo "ansible ALL=(ALL) NOPASSWD:ALL" \
        > /etc/sudoers.d/ansible && \
    chmod 0440 /etc/sudoers.d/ansible

Instalo la clave SSH:

RUN mkdir -p /home/ansible/.ssh

COPY ssh/ansible_lab.pub /home/ansible/.ssh/authorized_keys

RUN chown -R ansible:ansible /home/ansible/.ssh && \
    chmod 700 /home/ansible/.ssh && \
    chmod 600 /home/ansible/.ssh/authorized_keys

Habilito SSH:

RUN systemctl enable sshd

Y arranco el contenedor con systemd:

STOPSIGNAL SIGRTMIN+3

CMD ["/usr/lib/systemd/systemd"]

La principal diferencia frente a Debian es el uso de dnf y del servicio sshd.

5. Creo el Dockerfile de Alpine

Alpine no utiliza systemd, sino OpenRC, por lo que su imagen es más sencilla.

Creo:

vim images/alpine/Dockerfile

Parto de Alpine 3.24:

FROM alpine:3.24

RUN apk add --no-cache \
    openrc \
    openssh-server \
    openssh-server-common-openrc \
    sudo \
    python3 \
    bash \
    procps \
    iproute2 \
    iputils \
    curl

Creo el usuario:

RUN adduser -D -s /bin/sh ansible && \
    passwd -d ansible && \
    echo "ansible ALL=(ALL) NOPASSWD:ALL" \
        > /etc/sudoers.d/ansible && \
    chmod 0440 /etc/sudoers.d/ansible

Añado la clave pública:

RUN mkdir -p /home/ansible/.ssh

COPY ssh/ansible_lab.pub /home/ansible/.ssh/authorized_keys

RUN chown -R ansible:ansible /home/ansible/.ssh && \
    chmod 700 /home/ansible/.ssh && \
    chmod 600 /home/ansible/.ssh/authorized_keys

Habilito SSH con OpenRC:

RUN rc-update add sshd default

Y preparo el arranque:

RUN mkdir -p /usr/local/sbin && \
    printf '%s\n' \
        '#!/bin/sh' \
        'ssh-keygen -A' \
        'exec /sbin/init' \
        > /usr/local/sbin/ansible-lab-init && \
    chmod 0755 /usr/local/sbin/ansible-lab-init

CMD ["/usr/local/sbin/ansible-lab-init"]

Este nodo no necesita privileged ni configuración especial de cgroups.

6. Creo el Dockerfile de MySQL

Para MySQL parto de la imagen oficial:

vim images/mysql/Dockerfile
FROM mysql:8.4

Instalo lo necesario para administrarlo mediante Ansible:

RUN microdnf install -y \
    openssh-server \
    sudo \
    python3 \
    shadow-utils \
    passwd \
    procps-ng \
    iproute && \
    microdnf clean all

Creo el usuario ansible e instalo la clave exactamente igual que en el resto de nodos.

En este caso no quiero sustituir el arranque normal de MySQL, así que creo:

vim images/mysql/entrypoint-lab.sh

con:

#!/bin/bash

set -e
ssh-keygen -A
mkdir -p /run/sshd /usr/sbin/sshd
exec /usr/local/bin/docker-entrypoint.sh "$@"

Le doy permisos:

chmod +x images/mysql/entrypoint-lab.sh

Y desde el Dockerfile lo utilizo como wrapper:

COPY images/mysql/entrypoint-lab.sh \
     /usr/local/bin/entrypoint-lab.sh

RUN chmod 0755 /usr/local/bin/entrypoint-lab.sh

ENTRYPOINT ["/usr/local/bin/entrypoint-lab.sh"]

CMD ["mysqld"]

Así primero arranco SSH y después dejo que la imagen oficial continúe arrancando MySQL normalmente.

7. Construyo las imágenes

Desde la raíz del proyecto:

docker build \
  -t ansible-lab-debian:13-systemd \
  -f images/debian/Dockerfile.systemd .
docker build \
  -t ansible-lab-rocky:9-systemd \
  -f images/rocky/Dockerfile.systemd .
docker build \
  -t ansible-lab-alpine:3.24 \
  -f images/alpine/Dockerfile .
docker build \
  -t ansible-lab-mysql:8.4 \
  -f images/mysql/Dockerfile .

Compruebo el resultado:

docker image ls | grep ansible-lab

8. Creo las variables de MySQL

En .env:

MYSQL_ROOT_PASSWORD=ansible-lab-root
MYSQL_DATABASE=ansible_lab

Si publico el proyecto en GitHub, añado .env a .gitignore.

9. Creo Docker Compose

En compose.yml defino los seis nodos:

services:

  debian1:
    image: ansible-lab-debian:13-systemd
    container_name: ansible-debian1
    hostname: debian1
    privileged: true
    cgroup: private
    restart: "no"
    networks:
      ansible_lab:
        ipv4_address: 172.30.10.11

  debian2:
    image: ansible-lab-debian:13-systemd
    container_name: ansible-debian2
    hostname: debian2
    privileged: true
    cgroup: private
    restart: "no"
    networks:
      ansible_lab:
        ipv4_address: 172.30.10.12

  rocky1:
    image: ansible-lab-rocky:9-systemd
    container_name: ansible-rocky1
    hostname: rocky1
    privileged: true
    cgroup: private
    restart: "no"
    networks:
      ansible_lab:
        ipv4_address: 172.30.10.21

  rocky2:
    image: ansible-lab-rocky:9-systemd
    container_name: ansible-rocky2
    hostname: rocky2
    privileged: true
    cgroup: private
    restart: "no"
    networks:
      ansible_lab:
        ipv4_address: 172.30.10.22

  alpine1:
    image: ansible-lab-alpine:3.24
    container_name: ansible-alpine1
    hostname: alpine1
    restart: "no"
    networks:
      ansible_lab:
        ipv4_address: 172.30.10.31

  mysql1:
    image: ansible-lab-mysql:8.4
    container_name: ansible-mysql1
    hostname: mysql1
    restart: "no"

    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}

    volumes:
      - mysql-data:/var/lib/mysql

    networks:
      ansible_lab:
        ipv4_address: 172.30.10.40

networks:
  ansible_lab:
    name: ansible-lab
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.10.0/24
          gateway: 172.30.10.1

volumes:
  mysql-data:
    name: ansible-lab-mysql-data

Compruebo que Compose interpreta correctamente el archivo:

docker compose config

10. Arranco el laboratorio

docker compose up -d

Compruebo los contenedores:

docker compose ps

Y la red:

docker network inspect ansible-lab

Debo tener:

172.30.10.11    debian1
172.30.10.12    debian2
172.30.10.21    rocky1
172.30.10.22    rocky2
172.30.10.31    alpine1
172.30.10.40    mysql1

11. Compruebo SSH

Pruebo uno de los nodos:

ssh -i ~/.ssh/ansible_lab_ed25519 \
  ansible@172.30.10.11

Dentro compruebo:

hostname
whoami
sudo whoami
python3 --version

Después puedo repetir la prueba con el resto de direcciones.

Para Debian y Rocky también compruebo:

systemctl is-system-running

y espero:

running

En Alpine:

rc-service sshd status

Una vez comprobado SSH, Python y sudo en todos los nodos, el laboratorio está preparado para empezar a utilizar Ansible.

Para apagarlo:

docker compose down

Para volver a levantarlo:

docker compose up -d

A partir de aquí el siguiente paso será instalar Ansible en Manjaro, crear el inventario y comprobar todos los hosts con:

ansible all -m ping