Cómo usar los playbooks con Ansible en Linux

Publicado el 15 enero 2018 por Drassill
Continuando con el anterior artículo, en esta ocasión vamos a profundizar un poco más en el uso de la herramienta Ansible. Hasta ahora hemos podido mandar ordenes individuales desde el controlador al resto de equipos, a los que llamaremos nodos, pero el problema radica en que por el momento lo único que estamos haciendo es mandar una orden individual a todos... Una orden que puede ser muy práctica y que puede ahorrarnos mucho tiempo, pero que no es lo más práctico cuando queremos gestionar varios servicios y tareas... Es por ello que en este aparado vamos a centrarnos en uno de los aspectos más importantes que ofrece Ansible: Los playbooks. Antes de continuar, es recomendable que, si no se tienen noción alguna de Ansible, os leáis el anterior artículo.

Un playbook no es más que un fichero que contiene las ordenes correspondientes que queremos enviar a todos los nodos; ordenes que se irían ejecutando secuencialmente según hallamos escrito en el fichero. Dicho playbook, estará escrito en lenguaje YMAL y tendrá una extensión .yml. La lógica es parecida a la seguida con las ordenes y los módulos usados en el anterior apartado, pero con más opciones y con la posibilidad de crear un conjunto de ordenes de forma estructurada y ordenada. La mejor forma de entenderlo es con un pequeño ejemplo inicial. Para ello, para tener todo bien organizado, vamos a crear un directorio llamado PLAYBOOKS dentro de /etc/ansible y dentro de dicho directorio vamos a crear un fichero llamado master.yml.
mkdir /etc/ansible/PLAYBOOKS
touch /etc/ansible/PLAYBOOKS/master.yml
Dicho fichero tendrá el contenido de a continuación:
---
#Este es el playbook maestro
- hosts: all
remote_user: root
tasks:
- name: Asegurarse que NTP esta en marcha
service: name=ntp state=started enabled=yes
...

Puede parecer complicado, pero si lo desgranamos, veremos que es muy sencillo:
  • Los primeros tres guiones indican el comienzo del fichero. Todo fichero YMAL debe llevarlo para que Ansible pueda leerlo correctamente.
  • Cualquier línea que comience con una # será considerado un comentario, tal y como ocurre en los scripts de bash.
  • Después comenzaremos con la primera sección de todas, que sería de a qué grupo (especificado previamente dentro del fichero /etc/ansible/hosts) queremos hacer referencia. Allí podemos poner el nombre de un grupo que hayamos creado o, en caso de quererlo, decir que se envíen a todas las IPs y hosts que aparezcan dentro del fichero. En nuestro caso hemos optado por dicha opción, lo cual se representa mediante - hosts: all. Es muy importante comenzar con dicho - pues marcaría el inicio de las acciones y reglas dirigidas a dicho grupo.
  • Tras decir eso, habría que decir a qué usuario remoto nos queremos conectar. Aquí podemos escoger el que queramos, pero en mi caso, ya que he preparado el usuario root del otro equipo para poder conectarme directamente a él sin contraseña alguna, optaré por usar dicho usuario.
  • Ahora habría que pasar a la parte de las tareas... Dichas tareas se definen mediante, el nombre tasks: y después debajo de éste empezaríamos a mencionar las tareas una por una; tareas que siempre empezarían con un -.
  • Toda tarea está compuesta por, cómo mínimo, 2 partes. La primera parte sería meramente informativa. Se pondría un nombre descriptivo que nos mostraría Ansible antes de ejecutar la tarea. Dicho nombre se especificaría mediante - name. La segunda parte sería la tarea a ejecutar, que constaría de: Módulo: argumentos. En este caso hemos usado el módulo service, y los argumentos usados serían name, state y enabled. Se pueden poner tantas tareas como se quieran, siempre y cuando se siga la misma estructura que la que acabo de mencionar.
  • Por último, siempre al final de cada playbook habría que escribir tres puntos.

Este ejemplo es muy sencillo y puede evolucionar a mucho más, poniendo diferentes hosts y tareas, pero sirve de buena base introductoria... Para ejecutar dicho playbook, simplemente habría que escribir el comando:
ansible-playbook /etc/ansible/PLAYBOOKS/master.yml
El contenido del playbook, al hacer referencia a una única tarea, sería lo equivalente al siguiente comando de Ansible:
ansible all -m service -a \
"name=ntp state=started enabled=yes"
Vamos a poner un ejemplo más completo en esta ocasión; supongamos que queramos asegurarnos de que el servicio NTP tenga la última versión; además también vamos a verificar que el servicio Asterisk está en marcha. Por otro lado vamos a crear una tarea exclusiva para el grupo llamado PRUEBAS; una tarea que consistirá en revisar que tenemos la última versión del paquete apache2. Al final sería aplicara los mismos conceptos que hemos visto arriba, pero de forma un poco más amplia, dejando el fichero con el siguiente aspecto:
---
#Este es el playbook maestro
#Reglas para todos los hosts
- hosts: all
remote_user: root
tasks:
- name: Asegurarse que NTP esta en marcha
service: name=ntp state=started enabled=yes
- name: Verificar ultima version NTP
apt: name=ntp state=latest
- name: Asegurarse que Asterisk esta en marcha
service: name=asterisk state=started enabled=yes
# Reglas para el grupo PRUEBAS
- hosts: PRUEBAS
remote_user: root
tasks:
- name: Verificar ultima version Apache2
service: name=apache2 state=started enabled=yes
...

Como podéis ver siempre mantendrá la misma lógica... Es decir primero irían los hosts de destino, luego el usuario con el que queremos hacer las acciones y por último las tareas a realizar.
Una característica muy interesante que podemos aprovechar en los playbooks, es el uso de handlers. Un handler es una tarea que se ejecuta únicamente en caso de que una tarea concreta (especificada por nosotros) haya realizado un cambio de estado. Por ejemplo si tuviésemos un handler preparado para cuando hubiese un cambio de estado en Apache2, y una tarea que se asegurase de que apache2 tuviese la última versión, en caso de que Apache2 no la tuviese, el handler se ejecutaría. Veamos un ejemplo usando como referencia el mencionado Apache2:
---
#Este es el playbook maestro
- hosts: all
remote_user: root
tasks:
- name: Verificar ultima version Apache2
apt: name=apache2 state=latest
notify: "Reiniciar Apache2"
handlers:
- name: Reiniciar apache si es necesario
service: name=apache2 state=restarted
listen: "Reiniciar Apache2"
...

Como podéis observar, el handler está a la escucha de que le llegue una notificación; notificación que enviaríamos desde la tarea de verificación de la última versión de Apache2. Con lo que siempre que trabajásemos con handlers, trabajaríamos de la misma forma que con las tareas, pero con la diferencia de que desde la tarea tendríamos que notificar al handler mediante un notify, y desde el handler tendríamos que escuchar a que nos llegase una notificación concreta mediante un listen.
Gracias a las tareas (tasks) y handlers, podemos realizar playbooks muy completos donde podemos agrupar una enorme cantidad de tareas y handlers para luego así llamarlos a todos a la vez; ahorrándonos tener que escribir los comandos uno a uno para cada tarea que queramos realizar, haciendo que nos ahorremos muchísimo tiempo una vez tengamos todas las tareas correctamente definidas. Aún así, a nivel de gestión, el tener todas las tareas y handlers en un solo playbook puede ser una locura a nivel de mantenimiento... Cuando son pocas tareas y servicios no pasa nada, pero y si queremos gestionar 50 servicios, con diferentes grupos, handlers, etc... Técnicamente al agruparlo todo en un solo playbook es perfectamente viable, pero a nivel mantenerlo en el tiempo puede ser realmente costoso, especialmente si se quieren hacer cambios en el futuro... Es por ello que lo ideal es tener un playbook maestro y que este vaya llamando a diferentes playbooks dependiendo de las tareas que se quieran realizar. La llamada a dichos playbooks desde el playbook maestro se realiza mediante la sentencia include, y podemos crear una estructura perfectamente definida para que con el paso del tiempo se pueda mantener sin demasiados problemas.
Vamos a suponer que queremos poner un firewall para todos los equipos; un firewall común que todos van a tener; además también vamos a verificar que tanto Mysql como Apache2 están en marcha, pues tenemos un servidor web; por otro lado también verificaremos qué Asterisk tiene la última versión y que en dicho caso lo reiniciaremos. Estas tareas se podrían considerar como diferentes entre sí y por ello lo suyo sería tener: El playbook maestro, un playbook para el firewall, otro playbook para el apartado web y otro más para Asterisk. Comencemos con el aspecto del playbook maestro, master.yml:
---
#Este es el playbook maestro
- hosts: all
remote_user: root
tasks:
- include: /etc/ansible/PLAYBOOKS/Firewall.yml
- include: /etc/ansible/PLAYBOOKS/Web.yml
- include: /etc/ansible/PLAYBOOKS/Asterisk.yml
handlers:
- include: /etc/ansible/HANDLERS/Firewall_handler.yml
- include: /etc/ansible/HANDLERS/Web_handler.yml
- include: /etc/ansible/HANDLERS/Asterisk_handler.yml
...

Como podéis ver todo queda alojado en ficheros separados, tanto las tareas como los handlers; handlers que como podéis ver se alojarían en otro directorio llamado /etc/ansible/HANDLERS/. Estos ficheros tendrían una ligera variación con respecto a lo que hemos visto hasta ahora, pues a diferencia de un playbook "normal", en estos no haría falta ponerles las etiquetas, solamente las tareas a realizar, pues ya estarían asignadas previamente por el master.yml. Veamos por ejemplo el contenido de uno de los playbooks de tareas; concretamente el contenido del fichero Firewall.yml que posee un módulo que no hemos visto hasta ahora; el módulo copy en el que copiaremos un fichero desde nuestro controlador al resto de nodos.
---
#ESTE ES EL PLAYBOOK DEL FIREWALL
- name: COPIAR IPTABLES DEL CONTROLADOR A LOS NODOS
copy: src=/usr/src/iptables.sh dest=/etc/init.d/iptables.sh
notify: "CORTAFUEGOS"
...

Como veis directamente empezaríamos con el - name; sin mencionar los hosts, ni el usuario remoto, ni el hecho de que es una tarea y no un handler, pues todo eso ya ha sido especificado por el anterior playbook. En este caso lo que hacemos simplemente es copiar un fichero desde la máquina controladora al resto de equipos y que en caso de que haya una diferencia entre el iptables.sh del equipo controlador y el del nodo, mande un notify llamado "CORTAFUEGOS". Obviamente si el fichero iptables.sh no existiese en el nodo, también mandaría dicho notify.
El proceso de llamamiento de un handler mediante un include no difiere apenas con el realizado con las tareas. Buen ejemplo de ello sería el handler Firewall_handler.yml que estaría relacionado con la tarea arriba mostrada; en este caso usaremos otro módulo nuevo llamado file, capaz de modificar los permisos de un fichero para que sea ejecutable:
---
#HANDLERS FIREWALL
- name: Dar permisos firewall
file: dest=/etc/init.d/iptables.sh mode=755 state=touch
listen: "CORTAFUEGOS"
- name: Arrancar cortafuegos
shell: /etc/init.d/iptables.sh start
listen: "CORTAFUEGOS"
...

En este caso el handler lo primero que haría sería darle los permisos de ejecución necesarios al nuevo script para luego ejecutarlo. El contenido del cortafuegos no es algo relevante para este ejemplo, pero a modo de prueba podría ser algo como lo siguiente:
#!/bin/bash
case "$1" in
start)
iptables -F
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -m state --state NEW -j DROP
iptables -A OUTPUT -j ACCEPT
;;
stop)
iptables -F
;;
esac

Con esto ya tendríamos el firewall controlado y simplemente siguiendo las mismas pautas se podría hacer exactamente lo mismo para la gestión de tanto la parte Web como Asterisk, pues la base sería la misma que la que hemos estado usando hasta ahora.
Gracias a lo que hemos visto en este artículo, tendríamos un dominio básico de los playbooks gracias al cual podríamos realizar la mayoría de las gestiones necesarias para controlar un grupo de servidores... De aquí en adelante solo sería profundizar conceptos y jugar con los diferentes módulos de Ansible, con el fin de tener los playbooks más óptimos posibles.
Espero que os haya resultado útil.
Saludos.