Ansible es una herramienta de automatización de código abierto que configura servidores a través de SSH. No usa agentes: en los servidores gestionados solo hace falta Python, que Ubuntu ya incluye. En este tutorial instalarás Ansible en un nodo de control con Ubuntu 24.04, le darás acceso SSH a tus servidores, los describirás en un inventario, ejecutarás comandos ad-hoc y escribirás un primer playbook que instala y arranca Nginx.

Requisitos previos

Para seguir esta guía necesitas:

  • Un nodo de control: una máquina con Ubuntu 24.04 desde la que se ejecutará Ansible. Puede ser tu equipo de trabajo o un VPS pequeño de CubePath.
  • Uno o más nodos gestionados: servidores con Ubuntu 24.04 que Ansible configurará. Esta guía usa dos, web1 (web1_server_ip) y web2 (web2_server_ip).
  • Un usuario no root con privilegios sudo en cada nodo gestionado, llamado your_user en esta guía.
  • Acceso SSH (puerto 22) desde el nodo de control a todos los nodos gestionados.

Paso 1: Instalar Ansible en el nodo de control

Los repositorios de Ubuntu incluyen Ansible, pero con una versión antigua. El proyecto Ansible publica las versiones actuales en el PPA oficial ppa:ansible/ansible, que es compatible con Ubuntu 24.04.

En el nodo de control, añade el PPA e instala el paquete ansible:

sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible

El paquete ansible contiene ansible-core (el motor y los módulos integrados ansible.builtin) y un conjunto de colecciones como community.general y ansible.posix.

Comprueba la instalación:

ansible --version
ansible [core 2.18.6]
  config file = None
  configured module search path = ['/home/your_user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3/dist-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.12.3 (main, ...) [GCC 13.2.0]
  jinja version = 3.1.2

Los números de versión serán distintos en tu caso. En los nodos gestionados no hay que instalar nada.

Paso 2: Configurar la autenticación con claves SSH

Ansible se conecta a cada servidor por SSH, así que el nodo de control necesita acceso con clave, sin contraseña, a todos los nodos gestionados.

Si todavía no tienes una clave en el nodo de control, créala:

ssh-keygen -t ed25519 -C "ansible-control"

Pulsa ENTER para aceptar la ruta por defecto (~/.ssh/id_ed25519). Es recomendable usar una frase de paso; si la pones, carga la clave en el agente antes de usar Ansible con eval "$(ssh-agent)" y ssh-add.

Copia la clave pública a cada nodo gestionado:

ssh-copy-id your_user@web1_server_ip
ssh-copy-id your_user@web2_server_ip

Prueba la conexión. Debe iniciar sesión e imprimir el nombre del servidor sin pedir contraseña:

ssh your_user@web1_server_ip hostname
web1

Esta primera conexión también guarda la clave de host de cada servidor en ~/.ssh/known_hosts. Ansible verifica las claves de host por defecto, y conviene mantenerlo así.

Paso 3: Crear el directorio del proyecto y el inventario

El inventario le dice a Ansible qué servidores existen, cómo llegar a ellos y cómo se agrupan. Guárdalo en un directorio de proyecto junto con la configuración y los playbooks, para que todo el proyecto pueda vivir en Git.

Crea el directorio del proyecto:

mkdir ~/ansible-first-steps
cd ~/ansible-first-steps

Crea el archivo de inventario:

nano inventory.ini

Añade tus servidores a un grupo llamado webservers:

[webservers]
web1 ansible_host=web1_server_ip
web2 ansible_host=web2_server_ip

[webservers:vars]
ansible_user=your_user

web1 y web2 son los nombres que Ansible usa en su salida, ansible_host es la dirección a la que se conecta y ansible_user fija el usuario SSH para todos los hosts del grupo. Además, cada host pertenece al grupo implícito all.

Muestra lo que Ansible ve en el inventario:

ansible-inventory -i inventory.ini --graph
@all:
  |--@ungrouped:
  |--@webservers:
  |  |--web1
  |  |--web2

Paso 4: Añadir un archivo ansible.cfg

En lugar de pasar -i inventory.ini en cada comando, guarda los valores por defecto en un archivo ansible.cfg. Ansible lee el ansible.cfg del directorio actual cuando ejecutas los comandos desde la carpeta del proyecto.

nano ansible.cfg
[defaults]
inventory = inventory.ini
interpreter_python = auto_silent

[ssh_connection]
pipelining = True
  • inventory apunta al archivo de inventario.
  • interpreter_python = auto_silent deja que Ansible descubra Python en cada servidor sin mostrar un aviso.
  • pipelining reduce el número de operaciones SSH por tarea, lo que acelera bastante las ejecuciones.

Confirma que Ansible usa el archivo:

ansible --version | grep "config file"
  config file = /home/your_user/ansible-first-steps/ansible.cfg

Paso 5: Ejecutar comandos ad-hoc

Los comandos ad-hoc ejecutan un único módulo sobre un conjunto de hosts sin escribir un playbook. Son útiles para comprobaciones rápidas y tareas puntuales. La sintaxis es ansible <patrón> -m <módulo> -a "<argumentos>".

Comprueba la conectividad con el módulo ping. No envía paquetes ICMP: inicia sesión por SSH y verifica que Python funciona.

ansible all -m ping
web1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.12"
    },
    "changed": false,
    "ping": "pong"
}
web2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.12"
    },
    "changed": false,
    "ping": "pong"
}

Ejecuta un comando en todos los servidores web:

ansible webservers -m ansible.builtin.command -a "uptime"
web1 | CHANGED | rc=0 >>
 10:14:02 up 3 days,  2:11,  1 user,  load average: 0.00, 0.01, 0.00
web2 | CHANGED | rc=0 >>
 10:14:02 up 3 days,  2:09,  1 user,  load average: 0.02, 0.03, 0.00

Las tareas que necesitan root usan --become (-b). Añade -K (--ask-become-pass) para que Ansible te pida la contraseña de sudo una sola vez:

ansible webservers -b -K -m ansible.builtin.apt -a "update_cache=yes"

Recoge los facts, la información del sistema que Ansible obtiene de cada host, y fíltralos:

ansible web1 -m ansible.builtin.setup -a "filter=ansible_distribution*"
web1 | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "Ubuntu",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/os-release",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "24",
        "ansible_distribution_release": "noble",
        "ansible_distribution_version": "24.04"
    },
    "changed": false
}

Paso 6: Escribir tu primer playbook

Los comandos ad-hoc son imperativos: indicas qué ejecutar. Un playbook es declarativo: describes el estado que quieres y Ansible solo cambia lo que sea distinto. Los playbooks son archivos YAML que se pueden revisar, versionar y volver a ejecutar sin riesgo.

Crea un playbook que instala Nginx, se asegura de que está en marcha, abre HTTP en UFW y publica una página de inicio sencilla:

nano webserver.yml
---
- name: Configurar los servidores web
  hosts: webservers
  become: true

  tasks:
    - name: Instalar Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Arrancar Nginx y activarlo en el arranque
      ansible.builtin.systemd_service:
        name: nginx
        state: started
        enabled: true

    - name: Permitir SSH en UFW
      community.general.ufw:
        rule: allow
        name: OpenSSH

    - name: Permitir HTTP en UFW
      community.general.ufw:
        rule: allow
        port: "80"
        proto: tcp

    - name: Activar UFW
      community.general.ufw:
        state: enabled

    - name: Publicar la página de inicio
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        content: "<h1>Desplegado por Ansible en {{ inventory_hostname }}</h1>\n"
        owner: www-data
        group: www-data
        mode: "0644"

Algunos detalles importantes:

  • hosts: webservers apunta al grupo del inventario y become: true ejecuta todas las tareas con sudo.
  • Cada tarea llama a un módulo por su nombre completo (ansible.builtin.apt), lo que evita ambigüedades entre colecciones.
  • cache_valid_time: 3600 solo actualiza la caché de apt si tiene más de una hora.
  • La regla de OpenSSH va antes de state: enabled, así que activar el cortafuegos nunca te deja fuera.
  • {{ inventory_hostname }} es una variable Jinja2 con el nombre del host en el inventario.

Comprueba la sintaxis antes de ejecutarlo:

ansible-playbook webserver.yml --syntax-check
playbook: webserver.yml

Paso 7: Ejecutar el playbook

Empieza con una ejecución de prueba. --check informa de lo que cambiaría sin cambiar nada, y --diff muestra las diferencias en los archivos:

ansible-playbook webserver.yml -K --check --diff

En modo check, algunas tareas posteriores pueden dar error porque las anteriores no se ejecutaron de verdad (por ejemplo, el servicio de Nginx todavía no existe). Es normal en la primera ejecución.

Ahora aplica el playbook:

ansible-playbook webserver.yml -K

Introduce tu contraseña de sudo en el aviso BECOME password:. La ejecución termina con un resumen por host:

PLAY RECAP *********************************************************************
web1    : ok=7    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2    : ok=7    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

ok cuenta las tareas que se ejecutaron correctamente (incluida la recogida de facts) y changed las que modificaron algo.

Verifica el resultado desde el nodo de control:

curl http://web1_server_ip
<h1>Desplegado por Ansible en web1</h1>

Paso 8: Comprobar la idempotencia

Ejecuta de nuevo el mismo playbook:

ansible-playbook webserver.yml -K
PLAY RECAP *********************************************************************
web1    : ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2    : ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

changed=0 significa que los servidores ya coinciden con el estado descrito, así que Ansible no ha hecho nada. Esta propiedad, la idempotencia, es la que permite ejecutar los playbooks una y otra vez sin riesgo, por ejemplo de forma programada o después de añadir un servidor nuevo al inventario. También es la razón para preferir módulos específicos (apt, copy, systemd_service) en lugar de command o shell, que siempre informan de changed porque Ansible no sabe qué han hecho.

Solución de problemas

UNREACHABLE! ... Permission denied (publickey,password): la clave SSH no está instalada para el usuario que usa Ansible. Revisa ansible_user en el inventario y prueba con ssh your_user@web1_server_ip. Ejecuta Ansible con -vvv para ver el comando SSH exacto que lanza.

Missing sudo password: la tarea usa become y tu usuario necesita contraseña para sudo. Añade -K al comando.

Host key verification failed: la clave del servidor no está en ~/.ssh/known_hosts o ha cambiado (por ejemplo, tras reinstalar el servidor). Conéctate una vez con ssh para aceptarla, o elimina la entrada antigua con ssh-keygen -R web1_server_ip.

couldn't resolve module/action 'community.general.ufw': solo has instalado ansible-core. Instala la colección con ansible-galaxy collection install community.general, o instala el paquete completo ansible como en el paso 1.

Conclusión

Has instalado Ansible en un nodo de control, configurado el acceso con claves SSH, descrito tus servidores en un inventario, ejecutado comandos ad-hoc y aplicado un playbook idempotente que configura Nginx y el cortafuegos en varios servidores a la vez.

A partir de aquí, guarda el proyecto en Git, aprende a organizar configuraciones más grandes con plantillas y handlers en playbooks prácticos, y usa variables, bucles y condicionales para que el mismo playbook funcione en entornos distintos.