Ansible para Networking

Automatizar servidores con Ansible y automatizar equipos de red se parecen en la estructura de los playbooks, pero internamente funcionan de forma diferente.

En un servidor Linux, Ansible normalmente accede por SSH y ejecuta módulos mediante Python en el host remoto. Un switch, router o firewall normalmente no dispone de ese entorno. Los módulos de networking se ejecutan en el nodo de control y se comunican con el dispositivo mediante CLI/SSH, NETCONF o APIs HTTP.

En este tutorial parto de que Ansible ya está instalado y sabemos manejar inventarios, variables, Vault, Jinja2, roles y playbooks. El objetivo es trabajar directamente con automatización de red.


1. El modelo de trabajo

En networking hay cuatro piezas que debemos tener claras:

Ansible
   |
   +-- network_cli ---- SSH / CLI
   |
   +-- netconf -------- NETCONF / XML / YANG
   |
   +-- httpapi -------- REST API / API del fabricante
   |
   +-- colección ------ módulos específicos del NOS

Los tres transportes más habituales son: ansible_connection: nsible.netcommon.network_cli para CLI sobre SSH, ansible_connection: nsible.netcommon.netconf para NETCONF, y ansible_connection: nsible.netcommon.httpapi para equipos gestionados mediante API. Ansible mantiene estas conexiones de forma persistente, lo que evita establecer una sesión nueva para cada tarea.

Además necesitamos indicar qué sistema operativo de red estamos controlando: ansible_network_os: cisco.ios.ios`.

Por ejemplo:

ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios

Ansible ya sabe que debe abrir una sesión CLI y cómo interpretar los prompts y modos de configuración de Cisco IOS.


2. Colecciones que vamos a utilizar

La automatización moderna de red con Ansible está organizada principalmente mediante colecciones.

Un requirements.yml razonable para un entorno multi-vendor puede ser:

---
collections:

  - name: ansible.netcommon
  - name: ansible.utils

  - name: cisco.ios
  - name: cisco.nxos
  - name: cisco.iosxr

  - name: arista.eos

  - name: junipernetworks.junos

  - name: arubanetworks.aoscx

  - name: vyos.vyos

  - name: community.routeros

  - name: fortinet.fortios

  - name: paloaltonetworks.panos

  - name: check_point.mgmt

  - name: f5networks.f5_modules

Instalamos todo con:

ansible-galaxy collection install -r requirements.yml

Y comprobamos:

ansible-galaxy collection list

Conviene fijar versiones en un entorno de producción para evitar que una actualización cambie parámetros o comportamiento.

Una consideración actual es Junos: junipernetworks.junos dejó de formar parte del paquete Ansible a partir de Ansible 14, aunque la colección puede seguir instalándose separadamente.


3. Estructura del proyecto

Para un entorno real prefiero separar inventario, datos, templates y automatizaciones:

network-ansible/
│
├── ansible.cfg
├── requirements.yml
├── inventory.yml
│
├── group_vars/
│   ├── all.yml
│   ├── cisco_ios.yml
│   ├── cisco_nxos.yml
│   ├── arista.yml
│   ├── juniper.yml
│   └── vault.yml
│
├── host_vars/
│
├── playbooks/
│   ├── audit.yml
│   ├── backup.yml
│   ├── vlans.yml
│   ├── interfaces.yml
│   └── ntp.yml
│
├── templates/
│   ├── ios/
│   ├── eos/
│   └── junos/
│
└── roles/
    └── network_base/

El inventario describe los dispositivos. Las variables describen cómo conectarse y qué configuración queremos. Los playbooks deberían contener principalmente la lógica.


4. Inventario multi-vendor

Un inventario sencillo:

---
all:
  children:

    cisco_ios:
      hosts:
        sw01:
          ansible_host: 10.10.10.11
        sw02:
          ansible_host: 10.10.10.12

    cisco_nxos:
      hosts:
        nx01:
          ansible_host: 10.10.20.11

    arista:
      hosts:
        eos01:
          ansible_host: 10.10.30.11

    juniper:
      hosts:
        junos01:
          ansible_host: 10.10.40.11

    mikrotik:
      hosts:
        rtr01:
          ansible_host: 10.10.50.11

    vyos:
      hosts:
        vyos01:
          ansible_host: 10.10.60.11

Ahora definimos el transporte por fabricante.

group_vars/cisco_ios.yml:

---
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios

ansible_user: ansible

ansible_become: true
ansible_become_method: enable

Cisco IOS permite utilizar become con become_method: enable para entrar en modo privilegiado.

Para NX-OS:

---
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.nxos.nxos
ansible_user: ansible

Arista:

---
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: arista.eos.eos
ansible_user: ansible

Juniper:

---
ansible_connection: ansible.netcommon.netconf
ansible_network_os: junipernetworks.junos.junos
ansible_user: ansible

NETCONF debe estar habilitado previamente en el equipo.

VyOS:

---
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: vyos.vyos.vyos
ansible_user: ansible

MikroTik por SSH:

---
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: community.routeros.routeros
ansible_user: ansible

RouterOS también puede gestionarse directamente mediante su API, algo especialmente interesante cuando queremos realizar modificaciones estructuradas.

Las contraseñas nunca deberían estar directamente en estos archivos. Para eso utilizamos Vault:

ansible-vault create group_vars/vault.yml

5. El primer audit multi-vendor

Antes de modificar configuraciones conviene comprobar conectividad y obtener información básica.

Con los dispositivos basados en CLI podemos utilizar:

---
- name: Auditoria de dispositivos de red
  hosts:
    - cisco_ios
    - cisco_nxos
    - arista
    - vyos

  gather_facts: false

  tasks:

    - name: Ejecutar show version
      ansible.netcommon.cli_command:
        command: show version
      register: version

    - name: Mostrar resultado
      ansible.builtin.debug:
        var: version.stdout

ansible.netcommon.cli_command permite ejecutar comandos operativos sobre plataformas soportadas por network_cli, evitando utilizar un módulo diferente para cada fabricante cuando no es necesario.

Pero los comandos no siempre coinciden entre sistemas operativos.

Podemos almacenar el comando como dato:

Cisco:

show_interfaces_command: show ip interface brief

Arista:

show_interfaces_command: show ip interface brief

VyOS:

show_interfaces_command: show interfaces

Y mantener un único playbook:

- name: Estado de interfaces
  ansible.netcommon.cli_command:
    command: "{{ show_interfaces_command }}"
  register: interfaces

Este patrón es muy útil en entornos multi-vendor: separar qué queremos consultar de cómo lo implementa cada fabricante.


6. Módulos command y config

Prácticamente todas las colecciones de networking siguen un patrón parecido.

En Cisco IOS tenemos:

ios_command
ios_config
ios_facts

Arista:

eos_command
eos_config
eos_facts

NX-OS:

nxos_command
nxos_config
nxos_facts

VyOS:

vyos_command
vyos_config
vyos_facts

Los módulos command son para comandos operativos.

Ejemplo:

- name: Consultar vecinos OSPF
  cisco.ios.ios_command:
    commands:
      - show ip ospf neighbor
      - show ip route ospf

No debemos utilizarlos para entrar manualmente en configure terminal. Para modificar configuración utilizamos ios_config o, preferiblemente cuando exista, un resource module.

Ejemplo:

- name: Configurar NTP
  cisco.ios.ios_config:
    lines:
      - ntp server 10.10.1.10
      - ntp server 10.10.1.11

7. Resource Modules: la forma más interesante de automatizar red

Cuando existe un resource module prefiero utilizarlo antes que enviar líneas de configuración.

Por ejemplo:

- name: Configurar interfaz L3
  cisco.ios.ios_l3_interfaces:
    config:

      - name: GigabitEthernet1
        ipv4:

          - address: 10.20.30.1/30

    state: merged

Aquí no estamos escribiendo comandos Cisco. Estamos describiendo el estado deseado.

Los resource modules normalizan recursos como:

interfaces
L2 interfaces
L3 interfaces
VLAN
ACL
BGP
OSPF
LACP
LLDP
rutas
hostname
logging
NTP

dependiendo de la plataforma.

Además permiten trabajar con estados como:

merged
replaced
overridden
deleted
gathered
rendered
parsed

según el módulo.

Por ejemplo:

state: merged

añade o modifica únicamente lo indicado.

state: replaced

reemplaza la configuración del recurso indicado.

Y:

state: overridden

puede convertir lo declarado en Ansible en la fuente de verdad del recurso completo.

Este último debe usarse con mucho cuidado.


8. Crear VLAN: mismo objetivo, diferentes fabricantes

Cisco IOS / IOS-XE

- name: Crear VLAN
  cisco.ios.ios_vlans:
    config:

      - vlan_id: 120
        name: SERVIDORES

    state: merged

Cisco NX-OS

- name: Crear VLAN
  cisco.nxos.nxos_vlans:
    config:

      - vlan_id: 120
        name: SERVIDORES

    state: merged

NX-OS dispone de resource modules específicos para VLAN, BGP, interfaces, ACL, EVPN y otros recursos.

Arista EOS

- name: Crear VLAN
  arista.eos.eos_vlans:
    config:

      - vlan_id: 120
        name: SERVIDORES

    state: merged

Aquí vemos una de las ventajas de los resource modules: la estructura de datos puede ser prácticamente idéntica aunque cambie el fabricante.


9. Configurar un puerto de acceso

Cisco IOS:

- name: Configurar puerto L2
  cisco.ios.ios_l2_interfaces:
    config:

      - name: GigabitEthernet1/0/10
        mode: access
        access:
          vlan: 120

    state: merged

Podemos separar además la configuración física:

- name: Configurar interfaz
  cisco.ios.ios_interfaces:
    config:

      - name: GigabitEthernet1/0/10
        description: SERVER-01
        enabled: true

    state: merged

La intención queda mucho más clara que enviando:

interface Gi1/0/10
 description SERVER-01
 switchport mode access
 switchport access vlan 120
 no shutdown

Y además obtenemos idempotencia real a nivel del recurso.


10. Configuración mediante bloques CLI

No siempre existe un resource module para lo que necesitamos.

Entonces utilizamos el módulo config.

Por ejemplo:

- name: Configurar interfaz
  cisco.ios.ios_config:
    parents:
      - interface GigabitEthernet1/0/20

    lines:
      - description UPLINK-SW02
      - switchport mode trunk
      - switchport trunk allowed vlan 10,20,30,120

parents indica el contexto.

Ansible genera conceptualmente:

interface GigabitEthernet1/0/20
 description UPLINK-SW02
 switchport mode trunk
 switchport trunk allowed vlan 10,20,30,120

Esta técnica sigue siendo muy útil para configuraciones que no disponen de módulo específico.


11. Configuración genérica con cli_config

También existe una alternativa independiente del fabricante:

- name: Aplicar configuracion CLI
  ansible.netcommon.cli_config:
    config: |
      interface Loopback100
       description Ansible

cli_config permite enviar configuración textual mediante network_cli y puede generar backup antes de realizar el cambio.

También podemos utilizar una plantilla:

- name: Aplicar configuracion
  ansible.netcommon.cli_config:
    config: "{{ lookup('ansible.builtin.template', 'ios/base.j2') }}"
    backup: true

Es especialmente útil cuando queremos construir una capa propia de automatización multi-vendor.


12. Separar datos de configuración

Una buena automatización no debería tener los datos incrustados en el playbook.

Por ejemplo:

vlans:

  - id: 10
    name: USERS

  - id: 20
    name: VOICE

  - id: 30
    name: WIFI

  - id: 120
    name: SERVERS

El mismo modelo puede utilizarse para diferentes fabricantes.

Cisco:

- name: Configurar VLANs Cisco
  cisco.ios.ios_vlans:
    config: "{{ vlans | map('combine', {}) | list }}"
    state: merged

En proyectos grandes suelo llevar este concepto algo más lejos:

datos de red
     |
     v
modelo normalizado
     |
     +---- Cisco
     +---- Arista
     +---- Juniper
     +---- Aruba

La topología y la política de red son los datos. El fabricante solo determina cómo traducimos esos datos al equipo.


13. Jinja2 sigue siendo muy útil

Aunque existan resource modules, habrá configuraciones donde una plantilla sea más práctica.

Por ejemplo:

{% for vlan in vlans %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
{% endfor %}

Con los datos:

vlans:
  - id: 10
    name: USERS

  - id: 20
    name: VOICE

podemos generar:

vlan 10
 name USERS
vlan 20
 name VOICE

Y aplicarlo:

- name: Generar y aplicar configuracion
  ansible.netcommon.cli_config:
    config: "{{ lookup('ansible.builtin.template', 'ios/vlans.j2') }}"

No utilizaría Jinja para todo. Si existe un resource module estable para un recurso, normalmente es preferible utilizarlo.


14. Juniper Junos

Junos funciona especialmente bien con NETCONF.

La conexión:

ansible_connection: ansible.netcommon.netconf
ansible_network_os: junipernetworks.junos.junos

Una configuración sencilla:

- name: Configurar hostname
  junipernetworks.junos.junos_config:
    lines:
      - set system host-name junos01

También podemos realizar backup:

- name: Backup Junos
  junipernetworks.junos.junos_config:
    backup: true

NETCONF resulta interesante porque dejamos de automatizar una sesión de terminal y empezamos a trabajar mediante un protocolo diseñado específicamente para configuración de red.

Para dispositivos que implementen NETCONF estándar también podemos utilizar directamente:

- name: Aplicar configuracion NETCONF
  ansible.netcommon.netconf_config:
    content: "{{ lookup('file', 'config.xml') }}"

netconf_config trabaja con configuración XML y puede aprovechar capacidades como candidate y confirmed commit cuando el dispositivo las soporta.


15. Aruba AOS-CX

AOS-CX permite trabajar mediante REST API o CLI.

Un inventario para API puede utilizar:

ansible_network_os: arubanetworks.aoscx.aoscx
ansible_connection: arubanetworks.aoscx.aoscx

ansible_aoscx_validate_certs: false
ansible_aoscx_use_proxy: false
ansible_acx_no_proxy: true

Crear una VLAN:

- name: Crear VLAN 120
  arubanetworks.aoscx.aoscx_vlan:
    vlan_id: 120
    name: SERVERS
    description: Servers VLAN

También podemos utilizar SSH:

ansible_connection: ansible.netcommon.network_cli
ansible_network_os: arubanetworks.aoscx.aoscx

Aruba recomienda separar en plays diferentes las operaciones API y CLI cuando necesitamos utilizar ambos métodos sobre el mismo switch.


16. MikroTik RouterOS

Con RouterOS podemos elegir SSH o API.

Por SSH:

- name: Consultar recursos
  community.routeros.command:
    commands:
      - /system resource print

Los comandos SSH deben especificarse con la ruta completa:

/system resource print
/ip address print
/interface print

Para configuraciones más estructuradas prefiero la API.

Ejemplo de modificación:

- name: Configurar NAT
  community.routeros.api_modify:
    hostname: "{{ ansible_host }}"
    username: "{{ routeros_user }}"
    password: "{{ routeros_password }}"

    path: ip firewall nat

    data:

      - action: masquerade
        chain: srcnat
        comment: NAT to WAN
        out-interface-list: WAN

api_modify es idempotente para las rutas que soporta y dispone de check_mode y diff_mode.


17. FortiGate / FortiOS

En firewalls tiene mucho más sentido trabajar con la API que intentar automatizar la CLI.

Fortinet proporciona la colección:

fortinet.fortios

y actualmente recomienda httpapi frente al antiguo fortiosapi.

Ejemplo de interfaz:

- name: Configurar interfaz FortiGate
  fortinet.fortios.fortios_system_interface:
    vdom: root
    state: present

    access_token: "{{ fortigate_token }}"

    system_interface:
      name: port3
      alias: LAN-SERVERS
      ip: 10.20.30.1 255.255.255.0
      allowaccess:
        - ping
        - https

La colección dispone de módulos para interfaces, políticas, objetos, VPN, routing, SD-WAN, HA y gran parte de la configuración de FortiOS.


18. Palo Alto Networks

PAN-OS dispone de su propia colección:

ansible-galaxy collection install paloaltonetworks.panos

La automatización se realiza mediante la API de PAN-OS.

Podemos definir el dispositivo:

provider:
  ip_address: "{{ ansible_host }}"
  username: "{{ panos_user }}"
  password: "{{ panos_password }}"

Y crear un objeto:

- name: Crear objeto de red
  paloaltonetworks.panos.panos_address_object:
    provider: "{{ provider }}"

    name: SRV-WEB
    value: 10.20.30.100
    description: Web server
    tag:
      - PROD

A partir de ahí podemos automatizar reglas de seguridad, NAT, interfaces, zonas, objetos o configuración de Panorama.


19. Check Point

Check Point utiliza su Management API.

La colección actual es:

check_point.mgmt

Ejemplo:

- name: Crear objeto host
  check_point.mgmt.cp_mgmt_host:
    name: SRV-WEB
    ip_address: 10.20.30.100
    state: present
    auto_publish_session: true

Las operaciones se realizan contra el Management Server mediante Web Services API.

Esto permite automatizar objetos, grupos, reglas, NAT, capas de acceso y publicación de políticas.


20. F5 BIG-IP

Para BIG-IP tenemos:

f5networks.f5_modules

Por ejemplo, crear un pool:

- name: Crear pool
  f5networks.f5_modules.bigip_pool:
    name: WEB_POOL
    partition: Common
    lb_method: least-connections-member

    provider:
      server: "{{ ansible_host }}"
      user: "{{ f5_user }}"
      password: "{{ f5_password }}"
      validate_certs: false

  delegate_to: localhost

Podemos añadir miembros:

- name: Añadir servidor
  f5networks.f5_modules.bigip_pool_member:
    pool: WEB_POOL
    name: WEB01
    host: 10.20.30.101
    port: 443

    provider: "{{ f5_provider }}"

  delegate_to: localhost

En BIG-IP hay que distinguir entre modificar la configuración activa y guardarla de forma persistente; la documentación de F5 indica utilizar bigip_config cuando sea necesario guardar la configuración en disco.


21. Huawei y otros fabricantes NETCONF

No recomiendo construir una automatización nueva dependiendo de módulos abandonados simplemente porque en el pasado estuvieran incluidos en community.network.

community.network fue retirada del paquete Ansible 12.

En equipos Huawei que dispongan de NETCONF podemos plantear una automatización basada directamente en:

ansible_connection: ansible.netcommon.netconf

y:

- name: Aplicar configuracion NETCONF
  ansible.netcommon.netconf_config:
    content: "{{ lookup('template', 'huawei/interface.xml.j2') }}"

El XML utilizado dependerá del modelo YANG soportado por el equipo.

Este enfoque también sirve para otras plataformas que implementen NETCONF estándar aunque Ansible no tenga un módulo específico para cada función.


22. Backup antes de tocar nada

Una de las automatizaciones más útiles y fáciles de implementar es el backup masivo.

Para dispositivos soportados por network_cli:

---
- name: Backup de red
  hosts: network_cli_devices
  gather_facts: false

  tasks:

    - name: Backup running-config
      ansible.netcommon.cli_backup:
        dir_path: backups
        filename: "{{ inventory_hostname }}.cfg"

cli_backup proporciona un mecanismo común para guardar la configuración en el nodo de control.

También podemos generar backup como parte de un cambio:

- name: Aplicar configuracion
  ansible.netcommon.cli_config:
    config: "{{ lookup('template', 'config.j2') }}"

    backup: true

    backup_options:
      dir_path: backups
      filename: "{{ inventory_hostname }}.cfg"

23. --check y --diff

Antes de modificar una red real conviene acostumbrarse a ejecutar:

ansible-playbook playbooks/vlans.yml --check

y cuando el módulo lo soporte:

ansible-playbook playbooks/vlans.yml --check --diff

El resultado debe revisarse exactamente igual que revisaríamos un cambio manual antes de introducirlo.

Para un cambio importante mi secuencia habitual sería:

pre-check
   ↓
backup
   ↓
check/diff
   ↓
cambio
   ↓
post-check

No considero que una tarea haya terminado simplemente porque Ansible devuelva:

changed=1 failed=0

El estado operacional de la red debe validarse después.


24. Validaciones antes y después del cambio

Los módulos command permiten esperar a que aparezca una determinada condición.

Ejemplo Cisco:

- name: Esperar vecino OSPF
  cisco.ios.ios_command:
    commands:
      - show ip ospf neighbor

    wait_for:
      - result[0] contains FULL

    retries: 10
    interval: 5

Con esto el playbook no solo configura OSPF: comprueba que realmente aparece una adyacencia.

También podemos utilizar assert:

- name: Validar numero de vecinos
  ansible.builtin.assert:

    that:
      - ospf_neighbors | length >= 2

    fail_msg: "No se han formado todos los vecinos OSPF"

Aquí es donde la automatización empieza a resultar realmente interesante: pasamos de ejecutar comandos a comprobar el estado de la red.


25. Convertir CLI en datos estructurados

Uno de los problemas clásicos de networking es recibir:

GigabitEthernet1/0/1 up up
GigabitEthernet1/0/2 down down
GigabitEthernet1/0/3 up up

como texto.

Para automatizar necesitamos datos.

ansible.utils.cli_parse permite convertir salida CLI en estructuras que Ansible puede procesar mediante parsers nativos, TextFSM, ntc_templates, TTP, pyATS, XML o JSON.

Ejemplo:

- name: Obtener interfaces estructuradas
  ansible.utils.cli_parse:

    command: show interfaces

    parser:
      name: ansible.netcommon.ntc_templates

  set_fact: interfaces

Entonces podemos trabajar con:

- name: Detectar interfaces caidas
  ansible.builtin.debug:
    msg: "{{ item.interface }} esta DOWN"

  loop: "{{ interfaces }}"

  when:
    - item.link_status == "down"

Esto permite construir automatizaciones de troubleshooting, auditoría y compliance bastante más potentes.


26. Compliance de configuración

Supongamos que todos los switches deben cumplir estas condiciones:

required_ntp:
  - 10.10.1.10
  - 10.10.1.11

required_dns:
  - 10.10.2.10

required_syslog:
  - 10.10.3.10

Podemos obtener facts o configuración del equipo y compararlos con este modelo.

Conceptualmente:

%%{init: {
  "theme": "base",
  "themeVariables": {
    "fontFamily": "sans-serif",
    "fontSize": "13px",
    "edgeLabelBackground": "#ffffff"
  }
} }%%
flowchart TB
    classDef state fill:#2b3e50,stroke:#1a252f,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef process fill:#2980b9,stroke:#1f618d,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef decision fill:#e67e22,stroke:#d35400,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef ok fill:#16a085,stroke:#117a65,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef drift fill:#c0392b,stroke:#962d22,stroke-width:2px,color:#fff,rx:8,ry:8;

    DES["<b>CONFIGURACIÓN DESEADA</b>"]:::state
    ANS["<b>ANSIBLE</b><br/>Motor de Automatización"]:::process
    REAL["<b>CONFIGURACIÓN REAL</b><br/>Estado en el Nodo"]:::state
    CMP["<b>COMPARACIÓN</b><br/>Motor de Verificación"]:::process

    subgraph RESULTS ["Resultado del Estado"]
        direction LR
        OK["<b>OK</b><br/>Sin cambios / Sincronizado"]:::ok
        DRIFT["<b>DRIFT</b><br/>Diferencia detectada"]:::drift
    end

    DES --> ANS
    ANS --> REAL
    REAL --> CMP
    CMP --> OK
    CMP --> DRIFT

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

Esto convierte Ansible en una herramienta de control de configuración, no simplemente en una forma de ejecutar comandos más rápido.


27. Desplegar cambios por lotes

Nunca aplicaría inicialmente un cambio importante a 300 switches simultáneamente.

Podemos utilizar:

---
- name: Cambio progresivo
  hosts: access_switches

  gather_facts: false

  serial: 5

  tasks:

    - name: Aplicar cambio
      ...

Primero cinco equipos.

Si todo funciona, Ansible continúa con los siguientes.

Para un cambio especialmente sensible:

serial: 1

Un dispositivo cada vez.

También podemos limitar manualmente:

ansible-playbook change.yml --limit sw01

Después:

ansible-playbook change.yml --limit site_barcelona

Y finalmente:

ansible-playbook change.yml

Automatizar no significa desplegar cambios sin control.


28. Manejar timeouts

Los dispositivos de red pueden tardar bastante más que un servidor en responder a ciertas operaciones.

Podemos ajustar:

ansible_command_timeout: 60
ansible_connect_timeout: 30

O en ansible.cfg:

[persistent_connection]

command_timeout = 60
connect_timeout = 30

Ansible dispone específicamente de timeouts para las conexiones persistentes de networking.

Es algo especialmente habitual durante:

show tech
copias de firmware
commits grandes
consultas BGP
backups
operaciones API largas

29. Un playbook multi-vendor de verdad

Supongamos que queremos configurar VLAN 120 en Cisco IOS, NX-OS y Arista.

Los datos:

network_vlans:

  - vlan_id: 120
    name: SERVERS

Nuestro playbook puede ser:

---
- name: Configurar VLANs
  hosts: switches
  gather_facts: false

  tasks:

    - name: Cisco IOS
      cisco.ios.ios_vlans:
        config: "{{ network_vlans }}"
        state: merged

      when:
        - ansible_network_os == "cisco.ios.ios"

    - name: Cisco NXOS
      cisco.nxos.nxos_vlans:
        config: "{{ network_vlans }}"
        state: merged

      when:
        - ansible_network_os == "cisco.nxos.nxos"

    - name: Arista EOS
      arista.eos.eos_vlans:
        config: "{{ network_vlans }}"
        state: merged

      when:
        - ansible_network_os == "arista.eos.eos"

Funciona, pero cuando el proyecto crezca no dejaría esta lógica en un único playbook.

La movería a roles:

roles/
└── vlans/
    └── tasks/
        ├── main.yml
        ├── ios.yml
        ├── nxos.yml
        └── eos.yml

main.yml:

- name: Cargar tareas de plataforma
  ansible.builtin.include_tasks:
    file: "{{ platform }}.yml"

Y en cada grupo:

platform: ios

o:

platform: nxos

El playbook termina siendo:

---
- name: Configurar VLANs
  hosts: switches
  gather_facts: false

  roles:
    - vlans

La complejidad del fabricante queda encapsulada dentro del role.


30. Diseño recomendado para una automatización seria

Una arquitectura madura debería separar claramente cuatro capas:

%%{init: {
  "theme": "base",
  "themeVariables": {
    "fontFamily": "sans-serif",
    "fontSize": "13px",
    "edgeLabelBackground": "#ffffff"
  }
} }%%
flowchart TB
    classDef layer fill:#2b3e50,stroke:#1a252f,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef logic fill:#2980b9,stroke:#1f618d,stroke-width:2px,color:#fff,rx:8,ry:8;
    classDef device fill:#16a085,stroke:#117a65,stroke-width:2px,color:#fff,rx:8,ry:8;

    INV["<b>INVENTARIO</b><br/>Hosts y Grupos"]:::layer
    NET["<b>DATOS DE RED</b><br/>Variables y Parámetros"]:::layer
    ROLE["<b>ROLES / LÓGICA</b><br/>Tareas y Playbooks"]:::logic
    DRV["<b>DRIVERS / COLECCIONES</b><br/>Módulos de Conexión"]:::logic
    DEV["<b>DISPOSITIVOS</b><br/>Nodos Gestionados"]:::device

    INV ==> NET
    NET ==> ROLE
    ROLE ==> DRV
    DRV ==> DEV

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

El inventario dice:

qué dispositivos existen

Los datos dicen:

cómo debería ser la red

Los roles dicen:

qué operaciones hay que realizar

Las colecciones resuelven:

cómo hablar con cada fabricante

Eso permite cambiar un switch Cisco por un Arista sin tener que rediseñar todo el modelo de automatización.


31. Qué automatizar primero

En una red existente no empezaría desplegando BGP automáticamente.

Empezaría por tareas de riesgo bajo:

show commands
inventario
facts
versiones
serial numbers
interfaces
vecinos
CDP/LLDP
backups
auditorías
NTP
DNS
syslog
SNMP

Después pasaría a:

VLAN
interfaces
trunks
ACL
rutas
OSPF
BGP
firewalls
actualizaciones

Para entonces ya tendremos inventarios, modelos de datos, validaciones y backups suficientemente probados.


32. Flujo práctico de un cambio

Supongamos que tenemos que añadir la VLAN 120 en todos los switches de acceso.

Primero comprobamos dónde vamos a actuar:

ansible-inventory --graph

Probamos un único switch:

ansible-playbook playbooks/vlans.yml \
  --limit sw01 \
  --check \
  --diff

Ejecutamos:

ansible-playbook playbooks/vlans.yml \
  --limit sw01

Validamos:

ansible-playbook playbooks/audit_vlan.yml \
  --limit sw01

Probamos una sede:

ansible-playbook playbooks/vlans.yml \
  --limit site_barcelona

Y solo cuando el resultado es correcto desplegamos al grupo completo:

ansible-playbook playbooks/vlans.yml

Este método conserva algo que es fácil perder cuando empezamos a automatizar: el control sobre el cambio.


33. El objetivo final: gestionar intención, no comandos

El salto importante aparece cuando dejamos de pensar:

quiero ejecutar este comando

y empezamos a pensar:

quiero que esta sea la configuración de la red

En lugar de:

commands:
  - vlan 120
  - name SERVERS

definimos:

vlans:

  - vlan_id: 120
    name: SERVERS

En lugar de guardar cientos de configuraciones independientes:

sw01.cfg
sw02.cfg
sw03.cfg

guardamos el modelo de la red:

sites:

  barcelona:
    vlans:
      users: 10
      voice: 20
      wifi: 30
      servers: 120

Y dejamos que Ansible traduzca esa intención a Cisco, Arista, Juniper, Aruba, MikroTik o cualquier otra plataforma.

Ese es el punto donde Ansible deja de ser simplemente una forma de ejecutar comandos en muchos equipos y pasa a convertirse en una herramienta de automatización de red.