Conceptos básicos de Ansible, sin los cuales sus manuales serán un trozo de pasta pegajosa

Hago muchas revisiones del código Ansible de otras personas y escribo mucho yo mismo. Durante el análisis de los errores (tanto los de otras personas como los míos), así como en varias entrevistas, me di cuenta del principal error que cometen los usuarios de Ansible: se meten en cosas complejas sin dominar las básicas.

Para corregir esta injusticia universal, decidí escribir una introducción a Ansible para quienes ya lo conocen. Te lo advierto, esto no es un recuento de mans, es una lectura larga con muchas letras y sin imágenes.

El nivel esperado por el lector es que ya se han escrito varios miles de líneas de yamla, algo ya está en producción, pero “de alguna manera todo está torcido”.

Nombres

El principal error que comete un usuario de Ansible es no saber cómo se llama algo. Si no conoce los nombres, no podrá entender lo que dice la documentación. Un ejemplo vivo: durante una entrevista, una persona que parecía decir que escribía mucho en Ansible no pudo responder a la pregunta “¿de qué elementos consta un playbook?” Y cuando sugerí que “se esperaba la respuesta de que el libro de jugadas consiste en juego”, siguió el comentario condenatorio “no usamos eso”. La gente escribe Ansible por dinero y no usa Play. De hecho lo usan, pero no saben qué es.

Entonces comencemos con algo simple: ¿cómo se llama? Quizás lo sepas, o quizás no, porque no prestaste atención cuando leíste la documentación.

ansible-playbook ejecuta el libro de jugadas. Un playbook es un archivo con la extensión yml/yaml, dentro del cual hay algo como esto:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Ya nos dimos cuenta de que todo este archivo es un libro de jugadas. Podemos mostrar dónde están los roles y dónde están las tareas. ¿Pero dónde está el juego? ¿Y cuál es la diferencia entre juego y rol o playbook?

Todo está en la documentación. Y lo extrañan. Principiantes: porque hay demasiadas cosas y no recordarás todo a la vez. Experimentado - porque "cosas triviales". Si tiene experiencia, vuelva a leer estas páginas al menos una vez cada seis meses y su código será líder en su clase.

Así que recuerda: Playbook es una lista que consta de juegos y import_playbook.
Esta es una obra de teatro:

- hosts: group1
  roles:
    - role1

y esta también es otra obra:

- hosts: group2,group3
  tasks:
    - debug:

¿Qué es jugar? ¿Por qué esta ella?

El juego es un elemento clave para un libro de jugadas, porque jugar y solo jugar asocia una lista de roles y/o tareas con una lista de hosts en los que deben ejecutarse. En lo más profundo de la documentación se puede encontrar mención de delegate_to, complementos de búsqueda local, configuraciones específicas de CLI de red, hosts de salto, etc. Le permiten cambiar ligeramente el lugar donde se realizan las tareas. Pero olvídalo. Cada una de estas opciones inteligentes tiene usos muy específicos y definitivamente no son universales. Y estamos hablando de cosas básicas que todo el mundo debería saber y utilizar.

Si quieres realizar “algo” “en algún lugar”, escribes obra. No es un papel. No es un rol con módulos y delegados. Lo tomas y escribes play. En el cual, en el campo hosts, enumera dónde ejecutar y en roles/tareas, qué ejecutar.

Sencillo, ¿verdad? ¿Cómo podría ser de otra manera?

Uno de los momentos característicos en los que la gente desea hacer esto no a través del juego es el “papel que lo organiza todo”. Me gustaría tener un rol que configure tanto servidores del primer tipo como servidores del segundo tipo.

Un ejemplo arquetípico es el seguimiento. Me gustaría tener un rol de monitoreo que configurará el monitoreo. La función de monitoreo se asigna a los hosts de monitoreo (según el juego). Pero resulta que para monitorear necesitamos entregar paquetes a los hosts que estamos monitoreando. ¿Por qué no utilizar delegado? También necesitas configurar iptables. ¿delegar? También necesita escribir/corregir una configuración para el DBMS para habilitar el monitoreo. ¡delegar! Y si falta creatividad, entonces puedes hacer una delegación. include_role en un bucle anidado usando un filtro complicado en una lista de grupos, y dentro include_role puedes hacer más delegate_to de nuevo. Y allá vamos...

Un buen deseo, tener una única función de seguimiento que “lo haga todo”, nos lleva a un completo infierno del que, en la mayoría de los casos, sólo hay una salida: reescribir todo desde cero.

¿Dónde ocurrió el error aquí? En el momento en que descubrió que para realizar la tarea "x" en el host X tenía que ir al host Y y hacer "y" allí, tuvo que hacer un ejercicio simple: ir y escribir play, que en el host Y hace y. No agregues nada a "x", sino escríbelo desde cero. Incluso con variables codificadas.

Parece que todo lo expuesto en los párrafos anteriores está dicho correctamente. ¡Pero este no es tu caso! Porque desea escribir código reutilizable que sea SECO y similar a una biblioteca, y necesita buscar un método sobre cómo hacerlo.

Aquí es donde acecha otro grave error. Un error que convirtió muchos proyectos de estar bien escritos (podría ser mejor, pero todo funciona y es fácil de terminar) en un completo horror que ni siquiera el autor puede descifrar. Funciona, pero Dios no permita que cambies nada.

El error es: el rol es una función de biblioteca. Esta analogía ha arruinado tantos buenos comienzos que resulta sencillamente triste observarla. El rol no es una función de biblioteca. No puede hacer cálculos ni tomar decisiones a nivel de juego. ¿Recuérdame qué decisiones toma Play?

Gracias, tienes razón. El juego toma una decisión (más precisamente, contiene información) sobre qué tareas y roles realizar en qué hosts.

Si delegas esta decisión a un rol, e incluso con cálculos, te condenas a ti mismo (y a quien intentará analizar tu código) a una existencia miserable. El rol no decide dónde se desempeña. Esta decisión se toma jugando. El rol hace lo que se le dice, donde se le dice.

¿Por qué es peligroso programar en Ansible y por qué COBOL es mejor que Ansible? Hablaremos en el capítulo sobre variables y jinja. Por ahora, digamos una cosa: cada uno de sus cálculos deja un rastro indeleble de cambios en las variables globales y no puede hacer nada al respecto. Tan pronto como las dos “huellas” se cruzaron, todo desapareció.

Nota para los aprensivos: el rol ciertamente puede influir en el flujo de control. Comer delegate_to y tiene usos razonables. Comer meta: end host/play. ¡Pero! ¿Recuerdas que enseñamos lo básico? Me olvidé de delegate_to. Estamos hablando del código Ansible más simple y hermoso. Que es fácil de leer, fácil de escribir, fácil de depurar, fácil de probar y fácil de completar. Entonces, una vez más:

juega y solo el juego decide en qué host se ejecuta lo que se ejecuta.

En esta sección nos ocupamos de la oposición entre juego y rol. Ahora hablemos de la relación entre tareas y roles.

Tareas y roles

Considere jugar:

- hosts: somegroup
  pre_tasks:
    - some_tasks1:
  roles:
     - role1
     - role2
  post_tasks:
     - some_task2:
     - some_task3:

Digamos que necesitas hacer foo. Y parece foo: name=foobar state=present. ¿Dónde debería escribir esto? en pre? ¿correo? ¿Crear un rol?

...¿Y adónde fueron a parar las tareas?

Empezamos de nuevo con lo básico: el dispositivo de juego. Si flotas en este tema, no podrás usar el juego como base para todo lo demás y tu resultado será "inseguro".

Dispositivo de juego: directiva de hosts, configuración para el juego en sí y secciones pre_tasks, tareas, roles, post_tasks. Los demás parámetros de juego ya no son importantes para nosotros.

El orden de sus secciones con tareas y roles: pre_tasks, roles, tasks, post_tasks. Dado que semánticamente el orden de ejecución está entre tasks и roles no está claro, entonces las mejores prácticas dicen que estamos agregando una sección tasks, sólo si no roles. Si hay roles, luego todas las tareas adjuntas se colocan en secciones pre_tasks/post_tasks.

Lo único que queda es que todo está semánticamente claro: primero pre_tasksentonces rolesentonces post_tasks.

Pero todavía no hemos respondido la pregunta: ¿dónde está la llamada al módulo? foo ¿escribir? ¿Necesitamos escribir un rol completo para cada módulo? ¿O es mejor tener un papel importante para todo? Y si no es un rol, ¿dónde debería escribirlo: en el pre o en el post?

Si no hay una respuesta razonada a estas preguntas, entonces es un signo de falta de intuición, es decir, de esos mismos "cimientos inestables". Vamos a resolverlo. Primero, una pregunta de seguridad: si el juego tiene pre_tasks и post_tasks (y no hay tareas ni roles), ¿algo puede romperse si realizo la primera tarea desde post_tasks lo moveré hasta el final pre_tasks?

Por supuesto, la redacción de la pregunta da a entender que se romperá. ¿Pero qué exactamente?

... Manipuladores. La lectura de los conceptos básicos revela un hecho importante: todos los manipuladores se vacían automáticamente después de cada sección. Aquellos. todas las tareas de pre_tasks, luego todos los controladores que fueron notificados. Luego se ejecutan todos los roles y todos los controladores que fueron notificados en los roles. Después post_tasks y sus manejadores.

Por lo tanto, si arrastra una tarea desde post_tasks в pre_tasks, entonces potencialmente lo ejecutará antes de que se ejecute el controlador. por ejemplo, si en pre_tasks el servidor web está instalado y configurado, y post_tasks se le envía algo, luego transfiera esta tarea a la sección pre_tasks conducirá al hecho de que en el momento del "envío" el servidor aún no estará funcionando y todo se romperá.

Ahora pensemos de nuevo, ¿por qué necesitamos? pre_tasks и post_tasks? Por ejemplo, para completar todo lo necesario (incluidos los controladores) antes de cumplir el rol. A post_tasks nos permitirá trabajar con los resultados de la ejecución de roles (incluidos los controladores).

Un astuto experto en Ansible nos dirá de qué se trata. meta: flush_handlers, pero ¿por qué necesitamos flux_handlers si podemos confiar en el orden de ejecución de las secciones en juego? Además, el uso de meta:flush_handlers puede darnos cosas inesperadas con controladores duplicados, dándonos advertencias extrañas cuando se usa. when у block etc. Cuanto mejor conozca el ansible, más matices podrá nombrar para una solución "complicada". Y una solución sencilla (utilizar una división natural entre pre/roles/post) no plantea matices.

Y volvamos a nuestro 'foo'. ¿Dónde debería ponerlo? ¿En pre, post o roles? Obviamente, esto depende de si necesitamos los resultados del controlador para foo. Si no están allí, entonces no es necesario colocar foo ni en pre ni en post; estas secciones tienen un significado especial: ejecutar tareas antes y después del cuerpo principal del código.

Ahora la respuesta a la pregunta "rol o tarea" se reduce a lo que ya está en juego: si hay tareas allí, entonces debes agregarlas a las tareas. Si hay roles, debe crear un rol (incluso a partir de una tarea). Permítanme recordarles que las tareas y los roles no se utilizan al mismo tiempo.

Comprender los conceptos básicos de Ansible proporciona respuestas razonables a cuestiones aparentemente de gusto.

Tareas y roles (segunda parte)

Ahora analicemos la situación en la que recién estás comenzando a escribir un libro de jugadas. Necesitas hacer foo, bar y baz. ¿Son estas tres tareas, un rol o tres roles? Para resumir la pregunta: ¿en qué momento deberías empezar a escribir papeles? ¿Cuál es el punto de escribir roles cuando puedes escribir tareas?... ¿Qué es un rol?

Uno de los mayores errores (ya hablé de esto) es pensar que un rol es como una función en la biblioteca de un programa. ¿Cómo es la descripción de una función genérica? Acepta argumentos de entrada, interactúa con causas secundarias, genera efectos secundarios y devuelve un valor.

Ahora, atención. ¿Qué se puede hacer a partir de esto en el rol? Siempre puedes llamar a efectos secundarios, esta es la esencia de todo Ansible: crear efectos secundarios. ¿Tiene causas secundarias? Elemental. Pero con "pasar un valor y devolverlo", ahí es donde no funciona. Primero, no puedes pasar un valor a un rol. Puede establecer una variable global con un tamaño de vida útil en la sección vars para el rol. Puede establecer una variable global con una vida útil dentro del rol. O incluso con la vida útil de los libros de jugadas (set_fact/register). Pero no puedes tener "variables locales". No se puede "tomar un valor" y "devolverlo".

De esto se desprende lo principal: no se puede escribir algo en Ansible sin provocar efectos secundarios. Cambiar las variables globales es siempre un efecto secundario de una función. En Rust, por ejemplo, cambiar una variable global es unsafe. Y en Ansible es el único método para influir en los valores de un rol. Tenga en cuenta las palabras utilizadas: no "pasar un valor al rol", sino "cambiar los valores que usa el rol". No hay aislamiento entre roles. No existe aislamiento entre tareas y roles.

Total: un rol no es una función.

¿Qué tiene de bueno el papel? Primero, el rol tiene valores predeterminados (/default/main.yaml), en segundo lugar, la función tiene directorios adicionales para almacenar archivos.

¿Cuáles son los beneficios de los valores predeterminados? Porque en la pirámide de Maslow, la tabla bastante pervertida de prioridades variables de Ansible, los roles predeterminados son los de menor prioridad (menos los parámetros de la línea de comando de Ansible). Esto significa que si necesita proporcionar valores predeterminados y no preocuparse de que anulen los valores del inventario o las variables de grupo, entonces los valores predeterminados de roles son el único lugar adecuado para usted. (Estoy mintiendo un poco - hay más |d(your_default_here), pero si hablamos de lugares estacionarios, entonces solo roles predeterminados).

¿Qué más tienen de bueno los roles? Porque tienen sus propios catálogos. Estos son directorios para variables, tanto constantes (es decir, calculadas para el rol) como dinámicas (hay un patrón o un antipatrón). include_vars con {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Estos son los directorios de files/, templates/. Además, te permite tener tus propios módulos y complementos (library/). Pero, en comparación con las tareas de un libro de jugadas (que también puede tener todo esto), la única ventaja aquí es que los archivos no se agrupan en una sola pila, sino en varias pilas separadas.

Un detalle más: puedes intentar crear roles que estarán disponibles para su reutilización (vía galaxia). Con la llegada de las colecciones, la distribución de roles puede considerarse casi olvidada.

Por lo tanto, los roles tienen dos características importantes: tienen valores predeterminados (una característica única) y le permiten estructurar su código.

Volviendo a la pregunta original: ¿cuándo hacer tareas y cuándo hacer roles? Las tareas en un libro de jugadas se utilizan con mayor frecuencia como "pegamento" antes/después de los roles o como un elemento de construcción independiente (entonces no debería haber roles en el código). Un montón de tareas normales mezcladas con roles es un descuido inequívoco. Debes ceñirte a un estilo específico, ya sea una tarea o un rol. Los roles proporcionan separación de entidades y valores predeterminados, las tareas le permiten leer el código más rápido. Por lo general, se asigna más código “estacionario” (importante y complejo) a los roles y los scripts auxiliares se escriben en estilo de tarea.

Es posible realizar import_role como tarea, pero si escribes esto, prepárate para explicar a tu propio sentido de la belleza por qué quieres hacer esto.

Un lector astuto puede decir que los roles pueden importar roles, los roles pueden tener dependencias a través de galaxy.yml, y también hay una terrible y terrible include_role — Les recuerdo que estamos mejorando las habilidades en Ansible básico y no en gimnasia artística.

Manipuladores y tareas

Analicemos otra cosa obvia: los controladores. Saber utilizarlos correctamente es casi un arte. ¿Cuál es la diferencia entre un manejador y un arrastre?

Ya que recordamos lo básico, aquí hay un ejemplo:

- hosts: group1
  tasks:
    - foo:
      notify: handler1
  handlers:
     - name: handler1
       bar:

Los controladores del rol se encuentran en rolename/handlers/main.yaml. Los manejadores hurgan entre todos los participantes del juego: pre/post_tasks pueden sacar manejadores de roles, y un rol puede sacar manejadores de la obra. Sin embargo, las llamadas de "funciones cruzadas" a los controladores causan mucho más problemas que repetir un controlador trivial. (Otro elemento de las mejores prácticas es intentar no repetir los nombres de los controladores).

La principal diferencia es que la tarea siempre se ejecuta (idempotentemente) (etiquetas más/menos y when), y el controlador, por cambio de estado (notificar los incendios solo si se ha cambiado). ¿Qué quiere decir esto? Por ejemplo, el hecho de que cuando reinicie, si no hubo cambios, no habrá ningún controlador. ¿Por qué podría ser que necesitemos ejecutar el controlador cuando no hubo cambios en la tarea de generación? Por ejemplo, porque algo se rompió y cambió, pero la ejecución no llegó al manejador. Por ejemplo, porque la red estuvo temporalmente caída. La configuración ha cambiado, el servicio no se ha reiniciado. La próxima vez que lo inicie, la configuración ya no cambia y el servicio permanece con la versión anterior de la configuración.

La situación con la configuración no se puede resolver (más precisamente, puede inventar un protocolo de reinicio especial con indicadores de archivos, etc., pero esto ya no es un "ansible básico" de ninguna forma). Pero hay otra historia común: instalamos la aplicación, la grabamos .service-file, y ahora lo queremos daemon_reload и state=started. Y el lugar natural para esto parece ser el manejador. Pero si no lo convierte en un controlador sino en una tarea al final de una lista de tareas o rol, entonces se ejecutará de forma idempotente cada vez. Incluso si el libro de jugadas se rompiera por la mitad. Esto no resuelve en absoluto el problema del reinicio (no se puede realizar una tarea con el atributo de reinicio porque se pierde la idempotencia), pero definitivamente vale la pena hacerlo state=started, la estabilidad general de los playbooks aumenta, porque el número de conexiones y el estado dinámico disminuyen.

Otra propiedad positiva del controlador es que no obstruye la salida. No hubo cambios, no se omitieron ni aceptaron elementos adicionales en el resultado, es más fácil de leer. También es una propiedad negativa: si encuentra un error tipográfico en una tarea ejecutada linealmente en la primera ejecución, los controladores se ejecutarán solo cuando se modifiquen, es decir. bajo algunas condiciones, muy raramente. Por ejemplo, por primera vez en mi vida cinco años después. Y, por supuesto, habrá un error tipográfico en el nombre y todo se estropeará. Y si no los ejecuta la segunda vez, no se produce ningún cambio.

Por separado, debemos hablar de la disponibilidad de variables. Por ejemplo, si notificas una tarea con un bucle, ¿qué habrá en las variables? Puedes adivinar analíticamente, pero no siempre es trivial, especialmente si las variables provienen de lugares diferentes.

... Entonces los manejadores son mucho menos útiles y mucho más problemáticos de lo que parecen. Si puedes escribir algo hermoso (sin florituras) sin controladores, es mejor hacerlo sin ellos. Si no sale bonito, mejor con ellos.

El lector corrosivo señala con razón que no hemos discutido listenque un controlador puede llamar a notificar para otro controlador, que un controlador puede incluir import_tasks (que puede hacer include_role con with_items), que el sistema de controlador en Ansible es Turing-completo, que los controladores de include_role se cruzan de una manera curiosa con los controladores del juego, etc. .d. - Todo esto claramente no es lo "básico").

Aunque hay un WTF específico que en realidad es una característica que debes tener en cuenta. Si su tarea se ejecuta con delegate_to y ha notificado, entonces el controlador correspondiente se ejecuta sin delegate_to, es decir. en el host donde se asigna el juego. (Aunque el guía, por supuesto, puede tener delegate_to Mismo).

Por separado, quiero decir algunas palabras sobre los roles reutilizables. Antes de que aparecieran las colecciones, existía la idea de que se podían crear roles universales que pudieran ser ansible-galaxy install y fue. Funciona en todos los sistemas operativos de todas las variantes en todas las situaciones. Entonces mi opinión: no funciona. Cualquier papel con masa. include_vars, que admite 100500 casos, está condenado al abismo de errores en los casos extremos. Se pueden cubrir con pruebas masivas, pero como ocurre con cualquier prueba, o se tiene un producto cartesiano de valores de entrada y una función total, o se tienen “escenarios individuales cubiertos”. Mi opinión es que es mucho mejor si el rol es lineal (complejidad ciclomática 1).

Cuantos menos condicionales (explícitos o declarativos, en la forma when o forma include_vars por conjunto de variables), mejor será el rol. A veces hay que hacer ramas, pero, repito, cuantas menos haya, mejor. Entonces parece un buen papel con Galaxy (¡funciona!) con un montón de when puede ser menos preferible que el rol “propio” de cinco tareas. El momento en el que el papel con Galaxy es mejor es cuando empiezas a escribir algo. El momento en el que empeora es cuando algo se rompe y tienes la sospecha de que es por el “rol con la galaxia”. Lo abres y hay cinco inclusiones, ocho hojas de tareas y una pila. when'ov... Y tenemos que resolver esto. En lugar de 5 tareas, una lista lineal en la que no hay nada que romper.

En las siguientes partes

  • Un poco sobre inventario, variables de grupo, complemento host_group_vars, hostvars. Cómo hacer un nudo gordiano con espaguetis. Variables de alcance y precedencia, modelo de memoria Ansible. "Entonces, ¿dónde almacenamos el nombre de usuario de la base de datos?"
  • jinja: {{ jinja }} — plastilina blanda nosql notype nosense. Está en todas partes, incluso donde menos te lo esperas. un poco sobre !!unsafe y delicioso yaml.

Fuente: habr.com

Añadir un comentario