viernes, 24 de mayo de 2024

Ansible - Estructura de proyectos II

Hoy vamos a profundizar más en la estructura recomendada para un proyecto de Ansible. Como ya vimos, una estructura mínima que nos permite aprovechar ciertas características, como la encriptación mediante el uso de ansible-vault, era la siguiente:

Estructura mínima de proyecto.

Resumiendo lo expuesto anteriormente y por tanto, en el caso más simple:

  • En la carpeta raíz del proyecto, tendremos al menos el fichero de configuración ansible.cfg así como el fichero de inventario.
  • En la carpeta host_vars existirá un fichero que identificará, ya sea por nombre o dirección IP, a aquellos hosts que necesiten variables específicas.
  • En la carpeta group_vars existirán ficheros identificando a los grupos de hosts definidos en el fichero de inventario, y que contendrán variables comunes a dichos grupos de hosts.

Con una estructura así, podemos lanzar comandos ad-hoc simples teniendo todas las variables necesarias controladas y localizadas facilmente. De esta manera, podemos añadir o modificar las variables necesarias de forma simple y localizada, evitando así posibles duplicidades y errores. En resumen, con esta estructura:

  • Las variables comunes a grupos de hosts deben estar presentes en el fichero correspondiente al grupo, que debe encontrarse en la carpeta group_vars.
  • Es muy importante tener en cuenta que, al poder definir variables en múltiples sitios, es necesario que las variables tengan diferentes nombres. Ansible asigna un orden a las variables en función del ámbito de las mismas, lo cual puede llevar a situaciones en las que, variables con el mismo nombre, sean aplicadas según el orden de preferencia dado por Ansible llevando a resultados incorrectos en la ejecución del comando ad-hoc o playbook. En el siguiente enlace puedes consultar el orden que sigue Ansible para el uso de variables.

Como es lógico, todo esto son solo recomendaciones y dependerá del entorno en el que trabajemos, nuestra infraestructura y necesidades que tengamos.

Por tanto, con esta estructura podemos trabajar de forma cómoda con comandos ad-hoc, pero la potencia de Ansible es la posibilidad de crear playbooks con múltiples tareas. En ese caso, la estructura cambiará ligeramente ya que incluiremos el playbook en la carpeta raíz del proyecto, y nos quedaría algo como lo siguiente:

Proyecto con un playbook.

De forma muy simple, supongamos que tenemos que realizar ciertas tareas sobre un grupo de hosts, como por ejemplo:

  • Crear un nuevo usuario en el sistema.
  • Instalar un paquete.
  • Crear un directorio y aplicarle unos permisos determinados.
  • Copiar un fichero con cierta información.

Cada una de estas tareas, podemos realizarlas empleando comandos ad-hoc, pero, para que sea más sencillo y rapido realizar todas las tareas con una sola ejecución, podemos usar un playbook simple como el siguiente:

Playbook simple.

El playbook es un documento en formato YAML en el cual, si lo analizamos detenidamente, observamos lo siguiente:

  • Al ser un documento YAML, debe empezar siempre con tres guiones y terminar con tres puntos.
  • El playbook comienza con la especificación de su nombre, si queremos obtener los facts de todos los hosts, con la opción gather_facts, así como a que hosts queremos que se aplique, especificado con la opción hosts que, en este ejemplo, esta fijada en all.
  • Usando la cabecera tasks, definimos la lista de tareas que queremos que se apliquen a aquellos hosts sobre los que queramos lanzar el playbook.
  • Cada tarea está definida a continuación, especificando el nombre, módulo necesario y los parámetros del mismo.

Es importante darse cuenta del indentado o sangrado necesario, para asegurar que la sintaxis del playbook es correcta o Ansible devolverá un error al procesarlo.

Al usar un playbook, podemos hacer uso de características adicionales de Ansible como las siguientes:

  • Podemos usar el subdirectorio files, dentro de la carpeta del proyecto, para guardar aquellos ficheros estáticos que deben copiarse a los hosts. Al usar el módulo ansible.builtin.copy, Ansible buscará por defecto los ficheros a copiar dentro de dicha carpeta, lo que nos permite tener localizados de forma simple los ficheros cuyo contenido no cambia y que necesitamos copiar. En nuestro ejemplo tenemos lo siguiente:

Copia de fichero estático.

Como podemos ver, no especificamos la ruta donde se encuentra el fichero que queremos copiar, especificado por el parámetro src. Al usar un playbook, Ansible buscará la carpeta files y, si esta existe, buscará el fichero a copiar a las máquinas de destino. Para este ejemplo simple, dicho fichero está presente en la carpeta files y se copia a todos los hosts al ejecutar esta tarea.

  • Como vimos en entradas anteriores, usaremos variables, las cuales estarán presentes en el fichero de grupo correspondiente, dentro del directorio group_vars. En este caso, tenemos fijada la password del usuario que queremos crear con la variable secadmin_pass:

Creación de una cuenta de usuario.

Contenido del fichero group_vars/all.

De esta manera, en cada ejecución de la tarea para cada host, se creará el usuario con la password dada en el fichero. Adicionalmente, por simplicidad, he definido la password de SSH así como la de sudo en el fichero, para que no sea necesario especificarlas por línea de comandos.
  • De nuevo, por simplicidad, no he utilizado ansible-vault para encriptar las contraseñas, pero una de las ventajas de emplear esta estructura de directorios con un playbook es que podemos encriptar dicha información para protegerla, como ya vimos en una entrada anterior.

Por tanto, como vemos, emplear la estructura de directorios recomendada, simplifica un poco la creación de proyectos de Ansible y nos permite utilizar características como la encriptación mediante ansible-vault, copia de ficheros sin necesidad de especificar la ruta completa al fichero origen, etc...

En el caso de emplear templates, ficheros cuyo contenido cambia en función de una variable, o del resultado de la ejecución de una tarea anterior, añadiriamos la carpeta templates al directorio de proyecto, con lo que la estructura del mismo quedaría del siguiente modo:

Proyecto incluyendo la carpeta templates.

Mediante el uso de templates, Ansible puede copiar un fichero y al mismo tiempo modificar el contenido del mismo, mediante el uso de una o más variables. Veamoslo con un ejemplo práctico:

Uso de templates.

Como podemos ver, empleando el módulo ansible.builtin.setup, recogemos los facts de los hosts, los cuales quedan registrados en variables identificadas como ansible_ y posteriormente, en la siguiente tarea, usando el módulo ansible.builtin.template, copiamos el fichero especificado con el parámetro src. Este parámetro, que identifica el fichero que queremos que se copie, no incluye la ruta completa del mismo ya que se encuentra dentro de la carpeta templates del proyecto. El contenido de este fichero de plantilla puede ser algo como lo siguiente:
 
Contenido de un fichero de plantilla.

Cuando usamos el módulo template, Ansible copia el fichero a la ruta indicada por el parámetro dest y modifica el contenido del mismo, sustituyendo aquellas cadenas delimitadas con {{ }} por las variables que coincidan durante la ejecución del playbook con el nombre de las mismas. Por tanto, en este caso, al ejecutar el playbook, Ansible obtendrá los facts y registrará las variables correspondientes y, al ejecutar la tarea de copia usando el módulo template, realizará todas las sustituciones por las variables correspondientes.

Al ejecutar el playbook sobre un conjunto de contenedores, tenemos lo siguiente:

Ejecución de playbook simple.

Como podemos comprobar, cada una de las tareas se ha ejecutado en cada uno de los hosts gestionados y todas se han ejecutado correctamente. Analizando la salida vemos que, salvo la tarea Registrar facts de red, el estado del resto de tareas es changed. Cuando el resultado de una tarea es changed, Ansible nos indica que el sistema se ha cambiado, según las instrucciones descritas por la tarea. Sin embargo, cuando el resultado es ok, Ansible indica que el sistema ya se encontraba en el estado descrito por la tarea o, como en este caso, que se han recogido los datos correctamente.
 
Podemos comprobar que, efectivamente, el fichero que hemos utilizado como plantilla con el módulo template, se ha copiado y su contenido se ha modificado correctamente:
 
Resultado del uso de templates.

Para cada uno de los contenedores, se ha sustituido la cadena {{ ansible_hostname }} por el nombre del contenedor y la cadena {{ ansible_kernel }} por la versión de kernel, ambas obtenidas en la tarea anterior, en la que recogimos los facts de todos los hosts.

Como veremos más adelante, podríamos utilizar este proyecto de Ansible en un proyecto mayor encapsulándolo en lo que se denomina un rol. De forma muy simple, podemos entender un rol como el conjunto de tareas, junto con las variables, ficheros y plantillas necesarias, que describen el estado de un grupo de hosts. Esta idea, nos permitirá definir proyectos complejos que apliquen a muchos hosts, agrupados de tal manera que cada rol aplique solamente a los grupos de hosts que definamos, como servidores de aplicaciones, servidores web, firewalls, máquinas virtuales, etc. En esos casos, al ejecutar el playbook, podremos aplicar los roles correspondientes a cada grupo de forma eficiente en cada ejecución.

En próximas entradas analizaremos más detenidamente el concepto de rol para, más adelante, realizar un proyecto de configuración de hosts y emplear todo lo que hemos aprendido hasta ahora.