🐳 DevOps

Automatización de Infraestructura con Ansible: Guía Práctica

← Volver al Blog

1. Introducción

La automatización de infraestructura ha pasado de ser un lujo a una necesidad. Gestionar servidores manualmente con SSH no escala: es lento, propenso a errores y no deja trazabilidad. Herramientas como Puppet, Chef y Salt resuelven esto, pero Ansible se ha convertido en la opción preferida por su simplicidad y curva de aprendizaje reducida.

A diferencia de Puppet (Ruby DSL) o Chef (Ruby puro), Ansible usa YAML, legible incluso para quienes no escriben código diariamente. Salt requiere agente en los nodos gestionados; Ansible es agentless y funciona puramente sobre SSH. Esto reduce drásticamente la fricción de adopción.

En esta guía recorreremos desde la instalación hasta casos prácticos reales, como si estuvieras trabajando codo a codo con un ingeniero senior de infraestructura.

2. Arquitectura de Ansible

Entender la arquitectura es clave para diseñar automatizaciones correctas. Ansible sigue un modelo push-based sin agentes:

+----------------+       SSH (22/tcp)       +----------------+
|                |  --------------------->  |                |
|  Control Node  |                          |  Managed Node  |
|  (Ansible)     |  <---------------------  |  (Python 3)    |
|                |       stdout/stderr      |                |
+----------------+                          +----------------+

Ventajas del modelo agentless:

3. Instalación y Configuración Inicial

Instalación en Ubuntu/Debian

sudo apt update
sudo apt install -y ansible
ansible --version

Instalación en RHEL/CentOS/Rocky

sudo dnf install -y epel-release
sudo dnf install -y ansible
ansible --version

Configuración: ansible.cfg

Ansible busca configuración en este orden: ANSIBLE_CONFIG (variable de entorno) > ./ansible.cfg > ~/.ansible.cfg > /etc/ansible/ansible.cfg. Crea un archivo en tu proyecto:

[defaults]
inventory = ./inventory/hosts
host_key_checking = False
forks = 20
timeout = 30
stdout_callback = yaml
callback_whitelist = profile_tasks

[ssh_connection]
pipelining = True
control_path = /tmp/ansible-%%h-%%p-%%r

pipelining = True reduce drásticamente el número de conexiones SSH. Obligatorio en entornos con muchos nodos.

Inventario estático

# inventory/hosts
[webservers]
web01 ansible_host=192.168.1.10 ansible_user=deploy
web02 ansible_host=192.168.1.11 ansible_user=deploy

[dbservers]
db01 ansible_host=192.168.1.20 ansible_user=deploy

[monitoring]
monitor ansible_host=192.168.1.30

[production:children]
webservers
dbservers

Inventario dinámico con AWS EC2

# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
  - us-west-2
filters:
  tag:Environment: production
hostnames:
  - dns-name
  - private-ip
keyed_groups:
  - key: tags.Role
    prefix: role
compose:
  ansible_user: "'ec2-user'"

Se ejecuta con ansible-inventory -i inventory/aws_ec2.yml --graph. Los inventarios dinámicos se integran con cualquier proveedor que tenga API: AWS, GCP, Azure, VMware, OpenStack.

4. Comandos Ad-Hoc

Los comandos ad-hoc ejecutan tareas sin playbook. Ideales para diagnósticos rápidos y operaciones puntuales.

Ping de conectividad

ansible all -i inventory/hosts -m ping

Ejecutar comandos remotos

ansible webservers -m shell -a "uptime && free -h && df -h"

Copiar archivos

ansible webservers -m copy -a "src=./nginx.conf dest=/etc/nginx/nginx.conf mode=0644"

Gestionar servicios

ansible dbservers -m service -a "name=postgresql state=restarted enabled=yes"

Gestionar usuarios

ansible all -m user -a "name=jdoe state=present groups=sudo shell=/bin/bash create_home=yes"

Instalar paquetes

ansible webservers -m apt -a "name=nginx state=latest update_cache=yes" -b

Usa -b (become) para escalar privilegios. Combínalo con --become-user= si necesitas un usuario específico.

5. Playbooks

Los playbooks son el corazón de Ansible. Definen el estado deseado del sistema en YAML puro.

Sintaxis básica

---
- name: Configurar servidor web
  hosts: webservers
  become: yes
  vars:
    app_port: 3000
    app_env: production

  tasks:
    - name: Instalar Nginx
      apt:
        name: nginx
        state: latest
        update_cache: yes

    - name: Crear directorio de la aplicación
      file:
        path: /var/www/miapp
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'

    - name: Desplegar configuración de Nginx
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/miapp
      notify: reload nginx

Handlers

Los handlers solo se ejecutan si una tarea notifica el cambio, evitando reinicios innecesarios:

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

Variables y Facts

Ansible recopila facts del sistema automáticamente (IP, SO, memoria, discos). Puedes definir variables en varios niveles:

# group_vars/webservers.yml
---
nginx_port: 443
ssl_enabled: true
upstream_servers:
  - 10.0.1.10
  - 10.0.1.11
# host_vars/web01.yml
---
ansible_host: 192.168.1.10
app_version: 2.5.1

Usa facts en tus templates:

# templates/index.html.j2
<html>
<body>
  <h1>Servidor: {{ ansible_hostname }}</h1>
  <p>IP: {{ ansible_default_ipv4.address }}</p>
  <p>SO: {{ ansible_distribution }} {{ ansible_distribution_version }}</p>
</body>
</html>

Condicionales

    - name: Configurar firewall en Ubuntu
      ufw:
        rule: allow
        port: "{{ item }}"
      loop:
        - 80
        - 443
      when: ansible_os_family == "Debian"

    - name: Configurar firewall en RHEL
      firewalld:
        service: "{{ item }}"
        permanent: yes
        state: enabled
      loop:
        - http
        - https
      when: ansible_os_family == "RedHat"

Bucles

    - name: Crear usuarios del sistema
      user:
        name: "{{ item.username }}"
        groups: "{{ item.groups | default('users') }}"
        shell: "{{ item.shell | default('/bin/bash') }}"
        state: present
      loop:
        - { username: 'alice', groups: 'sudo' }
        - { username: 'bob', groups: 'docker' }
        - { username: 'charlie' }

6. Roles y Estructura de Proyectos

Un rol encapsula tareas, handlers, templates, variables y defaults en una estructura reutilizable. Es el equivalente a un "módulo" en Puppet o una "cookbook" en Chef.

Estructura estándar

roles/
  nginx/
    defaults/        # Variables por defecto (baja prioridad)
      main.yml
    vars/            # Variables sobreescribibles
      main.yml
    tasks/           # Tareas principales
      main.yml
    handlers/        # Handlers del rol
      main.yml
    templates/       # Templates Jinja2
      nginx.conf.j2
    files/           # Archivos estáticos
      index.html
    meta/            # Dependencias del rol
      main.yml

Crear un rol con ansible-galaxy

ansible-galaxy init roles/nginx
ansible-galaxy init roles/postgresql
ansible-galaxy init roles/monitoring

Playbook consumiendo roles

---
- name: Desplegar servidor web completo
  hosts: webservers
  become: yes

  roles:
    - role: common
    - role: nginx
      vars:
        nginx_port: 443
        ssl_cert: /etc/ssl/certs/server.crt
    - role: monitoring
      when: enable_monitoring | default(false)

Ansible Galaxy

Galaxy es el repositorio público de roles. En lugar de escribir un rol desde cero, busca si ya existe uno mantenido por la comunidad:

ansible-galaxy install geerlingguy.nginx
ansible-galaxy install geerlingguy.postgresql
ansible-galaxy install geerlingguy.firewall

Define dependencias en requirements.yml:

# requirements.yml
---
roles:
  - name: geerlingguy.nginx
    version: 3.1.4
  - name: geerlingguy.postgresql
    version: 3.5.0
  - name: geerlingguy.certbot
    version: 5.0.0
ansible-galaxy install -r requirements.yml

7. Caso Práctico: Despliegue de LAMP Stack

Construyamos un playbook completo que despliega Apache, MySQL y PHP con hardening básico.

Estructura del proyecto

lamp-deployment/
  ansible.cfg
  inventory/
    hosts
    group_vars/
      webservers.yml
      dbservers.yml
  requirements.yml
  site.yml
  roles/
    common/
    apache/
    mysql/
    php/

site.yml

---
- name: Configuración base de todos los servidores
  hosts: all
  become: yes
  roles:
    - common

- name: Desplegar servidores web Apache + PHP
  hosts: webservers
  become: yes
  roles:
    - apache
    - php

- name: Configurar base de datos MySQL
  hosts: dbservers
  become: yes
  roles:
    - mysql

Rol common: hardening base

# roles/common/tasks/main.yml
---
- name: Actualizar todos los paquetes
  apt:
    update_cache: yes
    upgrade: dist
    cache_valid_time: 3600

- name: Instalar herramientas base
  apt:
    name:
      - htop
      - curl
      - wget
      - git
      - ufw
      - fail2ban
      - unattended-upgrades
    state: present

- name: Configurar unattended-upgrades
  copy:
    src: 50unattended-upgrades
    dest: /etc/apt/apt.conf.d/50unattended-upgrades
    mode: '0644'

- name: Configurar firewall
  ufw:
    rule: "{{ item.rule }}"
    port: "{{ item.port | default(omit) }}"
    proto: "{{ item.proto | default('tcp') }}"
  loop:
    - { rule: 'allow', port: '22' }
  when: ansible_os_family == "Debian"

Rol apache

# roles/apache/tasks/main.yml
---
- name: Instalar Apache
  apt:
    name: apache2
    state: latest

- name: Habilitar módulos de seguridad
  apache2_module:
    name: "{{ item }}"
    state: present
  loop:
    - headers
    - rewrite
    - ssl
    - security

- name: Deshabilitar módulos innecesarios
  apache2_module:
    name: "{{ item }}"
    state: absent
  loop:
    - autoindex
    - status
    - info
  notify: restart apache

- name: Configurar cabeceras de seguridad
  template:
    src: security-headers.conf.j2
    dest: /etc/apache2/conf-available/security-headers.conf
  notify: restart apache

- name: Remover banner del servidor
  lineinfile:
    path: /etc/apache2/conf-enabled/security.conf
    regexp: '^ServerTokens'
    line: 'ServerTokens Prod'
  notify: restart apache
# roles/apache/handlers/main.yml
---
- name: restart apache
  service:
    name: apache2
    state: restarted
# roles/apache/templates/security-headers.conf.j2
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"

Rol mysql

# roles/mysql/tasks/main.yml
---
- name: Instalar MySQL
  apt:
    name:
      - mysql-server
      - python3-mysqldb
    state: latest

- name: Ejecutar mysql_secure_installation
  mysql_user:
    name: root
    host: "{{ item }}"
    password: "{{ mysql_root_password }}"
    check_implicit_admin: yes
    login_unix_socket: /var/run/mysqld/mysqld.sock
  loop:
    - 127.0.0.1
    - ::1
    - localhost

- name: Eliminar usuario anónimo
  mysql_user:
    name: ''
    host_all: yes
    state: absent

- name: Eliminar base de datos de prueba
  mysql_db:
    name: test
    state: absent

- name: Crear base de datos para la aplicación
  mysql_db:
    name: "{{ app_db_name }}"
    state: present

- name: Crear usuario de aplicación
  mysql_user:
    name: "{{ app_db_user }}"
    password: "{{ app_db_password }}"
    priv: "{{ app_db_name }}.*:ALL"
    host: '%'
    state: present

Rol php

# roles/php/tasks/main.yml
---
- name: Instalar PHP y extensiones
  apt:
    name:
      - php
      - php-mysql
      - php-curl
      - php-gd
      - php-mbstring
      - php-xml
      - php-xmlrpc
      - libapache2-mod-php
    state: latest
  notify: restart apache

- name: Configurar php.ini para producción
  template:
    src: php.ini.j2
    dest: /etc/php/{{ php_version }}/apache2/php.ini
  notify: restart apache
# roles/php/templates/php.ini.j2
expose_php = Off
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
max_execution_time = 30
memory_limit = 256M
post_max_size = 20M
upload_max_filesize = 20M
date.timezone = America/Bogota

Variables del proyecto

# group_vars/all.yml
---
mysql_root_password: "{{ vault_mysql_root_password }}"
app_db_name: miapp
app_db_user: miapp_user
app_db_password: "{{ vault_app_db_password }}"
php_version: 8.3

Ejecutar el despliegue completo

ansible-playbook -i inventory/hosts site.yml --ask-vault-pass

8. Ansible Vault

Nunca, bajo ninguna circunstancia, subas contraseñas o claves privadas a tu repositorio. Ansible Vault cifra archivos completos o variables individuales.

Cifrar un archivo entero

ansible-vault encrypt group_vars/all/vault.yml
ansible-vault view group_vars/all/vault.yml
ansible-vault edit group_vars/all/vault.yml
ansible-vault decrypt group_vars/all/vault.yml

Cifrar una variable individual

ansible-vault encrypt_string 'Str0ng!P4ss' --name 'mysql_root_password'

Salida:

mysql_root_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          61333931363438373766373036313334643939343861303866646662356162
          65366339376335306265333464323539356166633138386435653661376239
          366566383261310a3666386132623066663138373662333935353739376362
          36376361633938303630346633306664316262623438633638633730663463
          6137363131353232620a663434313261623163343132633437623965393763
          6437633239616430376238

Integración con CI/CD

Para entornos CI/CD, usa un archivo de contraseñas en lugar de pedirla interactivamente:

# .vault_pass (NUNCA subir al repo)
my_secret_vault_password
ansible-playbook site.yml --vault-password-file .vault_pass

En GitHub Actions, almacena la contraseña como secret y pásala con echo "${{ secrets.VAULT_PASS }}" > .vault_pass.

Múltiples vaults por entorno

group_vars/
  production/
    vault.yml        # Cifrado con vault-pass-prod
    vars.yml
  staging/
    vault.yml        # Cifrado con vault-pass-staging
    vars.yml
# ansible.cfg
[defaults]
vault_identity_list = production@.vault-pass-prod, staging@.vault-pass-staging

9. Integración con CI/CD

Ansible brilla cuando se integra en pipelines automatizados. Aquí no solo desplegamos configuraciones, sino que validamos, probamos y liberamos con trazabilidad.

GitHub Actions

# .github/workflows/deploy.yml
name: Deploy Infrastructure
on:
  push:
    branches: [main]
    paths:
      - 'ansible/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Instalar Ansible
        run: |
          sudo apt update
          sudo apt install -y ansible

      - name: Instalar roles de Galaxy
        run: ansible-galaxy install -r ansible/requirements.yml
        working-directory: ./ansible

      - name: Crear vault password
        run: echo "${{ secrets.VAULT_PASS }}" > .vault_pass

      - name: Check syntax
        run: ansible-playbook --syntax-check -i inventory/hosts site.yml
        working-directory: ./ansible

      - name: Dry-run (check mode)
        run: ansible-playbook -i inventory/hosts site.yml --check --diff
        working-directory: ./ansible

      - name: Ejecutar despliegue
        run: ansible-playbook -i inventory/hosts site.yml --vault-password-file .vault_pass
        working-directory: ./ansible

Jenkins Pipeline

// Jenkinsfile
pipeline {
    agent any
    environment {
        ANSIBLE_HOST_KEY_CHECKING = 'False'
        VAULT_PASS = credentials('vault-password')
    }
    stages {
        stage('Checkout') {
            steps { checkout scm }
        }
        stage('Install Ansible') {
            steps {
                sh 'sudo apt install -y ansible'
                sh 'ansible-galaxy install -r requirements.yml'
            }
        }
        stage('Syntax Check') {
            steps {
                sh 'ansible-playbook --syntax-check -i inventory/hosts site.yml'
            }
        }
        stage('Deploy') {
            steps {
                sh 'echo "$VAULT_PASS" > .vault_pass'
                sh 'ansible-playbook -i inventory/hosts site.yml --vault-password-file .vault_pass'
            }
        }
    }
}

AWX / Red Hat Ansible Automation Platform

AWX es la versión open-source del Automation Platform de Red Hat. Proporciona:

# Despliegue rápido de AWX con Docker Compose
git clone https://github.com/ansible/awx.git
cd awx/docker-compose
ansible-playbook -i inventory install.yml

10. Buenas Prácticas

Idempotencia

Un playbook es idempotente cuando ejecutarlo N veces produce el mismo resultado. Diseña cada tarea para que verifique el estado antes de actuar:

    # MAL: siempre reinicia
    - name: Reiniciar Nginx
      service:
        name: nginx
        state: restarted

    # BIEN: solo recarga si hay cambios
    - name: Recargar Nginx
      service:
        name: nginx
        state: reloaded

Testing con Molecule

Molecule es el framework de testing para roles Ansible. Aísla el rol en contenedores o VMs y ejecuta pruebas automatizadas:

pip install molecule molecule-plugins[docker]

# Iniciar proyecto de testing
molecule init scenario --driver-name docker

# molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: geerlingguy/docker-ubuntu2404-ansible:latest
    pre_build_image: true
provisioner:
  name: ansible
verifier:
  name: ansible
# Ejecutar pruebas
molecule create      # Crea contenedores
molecule converge    # Ejecuta el rol
molecule verify      # Verifica el estado
molecule destroy     # Limpia contenedores
molecule test        # Todo en uno

Version Control

Tagging

---
- name: Configuración completa
  hosts: all
  tasks:
    - name: Instalar paquetes base
      apt:
        name: "{{ item }}"
      loop:
        - curl
        - git
      tags:
        - packages
        - base

    - name: Configurar SSH
      template:
        src: sshd_config.j2
        dest: /etc/ssh/sshd_config
      tags:
        - ssh
        - security

    - name: Configurar firewall
      ufw:
        rule: allow
        port: 22
      tags:
        - firewall
        - security
# Ejecutar solo tareas específicas
ansible-playbook site.yml --tags security
ansible-playbook site.yml --tags packages --skip-tags security

Checklist de código limpio

PrácticaDescripción
✅ Nombres descriptivosInstalar dependencias de Python vs Task 1
✅ Usar módulos, no shell/commandPrefiere copy sobre shell: cp
✅ Variables con defaultsSiempre define valores por defecto en defaults/main.yml
✅ Secrets en VaultNunca texto plano en vars
✅ IdempotenciaVerifica estado, no asumas
✅ TestingMolecule + linting (ansible-lint)
✅ DocumentaciónCada rol con README describiendo variables y uso

Conclusión

Ansible es mucho más que "SSH con esteroides". Es una plataforma de automatización que, bien utilizada, permite gestionar desde un puñado de servidores hasta flotas completas en la nube con consistencia, trazabilidad y seguridad.

La clave está en empezar simple: inventario, algunos ad-hoc, un playbook pequeño. Luego roles, vault, CI/CD y testing. No intentes abarcar todo el ecosistema el primer día. Cada capa que agregues resolverá problemas reales de tu día a día como ingeniero de infraestructura.

El código de esta guía está listo para adaptar a tu entorno. La automatización no es el destino, es el camino.