Conceptos básicos de Ansible, sen os cales os teus libros de xogos serán un terrón de pasta pegajosa

Fago moitas revisións do código Ansible doutras persoas e escribo moito eu mesmo. Durante a análise de erros (tanto dos alleos como dos meus), así como dunha serie de entrevistas, decateime do principal erro que cometen os usuarios de Ansible: entran en cousas complexas sen dominar as básicas.

Para corrixir esta inxustiza universal, decidín escribir unha introdución a Ansible para quen xa o coñeza. Advírtovos que isto non é un relato de mans, é unha lectura longa con moitas letras e sen imaxes.

O nivel esperado do lector é que xa se escribiron varios miles de liñas de yamla, algo xa está en produción, pero "dalgunha maneira todo está torto".

Nomes

O principal erro que comete un usuario de Ansible é non saber como se chama algo. Se non coñeces os nomes, non podes entender o que di a documentación. Un exemplo vivo: durante unha entrevista, unha persoa que parecía dicir que escribía moito en Ansible non puido responder á pregunta "de que elementos consiste un libro de xogos?" E cando suxerín que "a resposta era de esperar que o libro de xogos consistía en xogar", seguiu o maldito comentario "non usamos iso". A xente escribe Ansible por diñeiro e non usa o xogo. En realidade úsano, pero non saben o que é.

Entón, imos comezar con algo sinxelo: como se chama. Quizais o saibas, ou quizais non, porque non fixeches caso cando leches a documentación.

ansible-playbook executa o playbook. Un playbook é un ficheiro coa extensión yml/yaml, dentro do cal hai algo así:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Xa nos demos conta de que todo este ficheiro é un playbook. Podemos mostrar onde están os papeis e onde están as tarefas. Pero onde está o xogo? E cal é a diferenza entre play e role ou playbook?

Todo está na documentación. E botan de menos. Principiantes - porque hai moito e non recordarás todo á vez. Experimentado - porque "cousas triviais". Se tes experiencia, volve ler estas páxinas polo menos unha vez cada seis meses e o teu código converterase en líder da clase.

Entón, recorda: Playbook é unha lista formada por play e import_playbook.
Esta é unha xogada:

- hosts: group1
  roles:
    - role1

e esta tamén é outra obra de teatro:

- hosts: group2,group3
  tasks:
    - debug:

Que é xogar? Por que é ela?

O xogo é un elemento clave para un libro de xogos, porque play and only play asocia unha lista de papeis e/ou tarefas cunha lista de hosts nos que se deben executar. No fondo da documentación pódese atopar mención delegate_to, complementos de busca local, configuracións específicas de cli de rede, servidores de salto, etc. Permítenche cambiar lixeiramente o lugar onde se realizan as tarefas. Pero, esquéceo. Cada unha destas intelixentes opcións ten usos moi específicos e definitivamente non son universais. E estamos a falar de cousas básicas que todos deberían coñecer e usar.

Se queres interpretar "algo" "nalgún lugar", escribes play. Non é un papel. Non un papel con módulos e delegados. Tómao e escribe xoga. No que, no campo hosts, enumera onde executar, e en roles/tarefas - que executar.

Simple, non? Como non podía ser doutro xeito?

Un dos momentos característicos nos que a xente ten o desexo de facelo non a través do xogo é o "papel que configura todo". Gustaríame ter un papel que configure tanto servidores do primeiro tipo como servidores do segundo tipo.

Un exemplo arquetípico é o seguimento. Gustaríame ter unha función de monitorización que configure a monitorización. A función de vixilancia atribúeselle aos anfitrións de seguimento (segundo o xogo). Pero resulta que para o seguimento necesitamos entregar paquetes aos hosts que estamos a supervisar. Por que non usar delegado? Tamén necesitas configurar iptables. delegado? Tamén cómpre escribir/corrixir unha configuración para o DBMS para habilitar a supervisión. delegado! E se falta creatividade, podes facer unha delegación include_role nun bucle anidado usando un filtro complicado nunha lista de grupos e dentro include_role podes facer máis delegate_to de novo. E marchamos...

Un bo desexo - ter un único papel de vixilancia, que "faga todo"- lévanos a un inferno completo do que a maioría das veces só hai unha saída: reescribir todo desde cero.

Onde ocorreu o erro aquí? No momento en que descubriches que para facer a tarefa "x" no host X había que ir ao host Y e facer "y" alí, tiñas que facer un exercicio sinxelo: ir e escribir play, que no host Y fai y. Non engadas nada a "x", pero escríbeo desde cero. Incluso con variables codificadas.

Parece que todo os parágrafos anteriores está dito correctamente. Pero este non é o teu caso! Porque queres escribir código reutilizable que sexa DRY e parecido á biblioteca, e necesitas buscar un método sobre como facelo.

Aquí é onde se esconde outro grave erro. Un erro que converteu moitos proxectos de tolerablemente escritos (podería ser mellor, pero todo funciona e é fácil de rematar) nun completo horror que nin o autor non pode descubrir. Funciona, pero Deus te libre de cambiar nada.

O erro é: rol é unha función da biblioteca. Esta analoxía arruinou tantos bos comezos que é simplemente triste de ver. O papel non é unha función da biblioteca. Non pode facer cálculos e non pode tomar decisións a nivel de xogo. Lémbrame que decisións toma o xogo?

Grazas, tes razón. Play toma unha decisión (máis precisamente, contén información) sobre que tarefas e roles realizar en que hosts.

Se delegas esta decisión nun papel, e mesmo con cálculos, condénate a ti mesmo (e a quen tentará analizar o teu código) a unha existencia miserable. O papel non decide onde se realiza. Esta decisión tómase xogando. O papel fai o que se lle di, onde se lle di.

Por que é perigoso programar en Ansible e por que COBOL é mellor que Ansible, falaremos no capítulo sobre variables e jinja. Por agora, digamos unha cousa: cada un dos teus cálculos deixa atrás un rastro indeleble de cambios nas variables globais e non podes facer nada ao respecto. En canto os dous "rastros" se cruzaron, todo desapareceu.

Nota para os squeamish: o papel certamente pode influír no fluxo de control. Comer delegate_to e ten usos razoables. Comer meta: end host/play. Pero! Lembras que ensinamos o básico? Esquecín delegate_to. Estamos a falar do código Ansible máis sinxelo e fermoso. Que é fácil de ler, fácil de escribir, fácil de depurar, fácil de probar e fácil de completar. Entón, unha vez máis:

xogar e só o xogo decide en que anfitrións o que se executa.

Neste apartado tratamos a oposición entre xogo e papel. Agora imos falar da relación tarefas vs roles.

Tarefas e roles

Considere o xogo:

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

Digamos que tes que facer foo. E parece foo: name=foobar state=present. Onde debo escribir isto? en pre? publicar? Crear un papel?

...E onde foron as tarefas?

Comezamos de novo cos conceptos básicos: o dispositivo de reprodución. Se flotas sobre este problema, non podes usar o xogo como base para todo o demais, e o teu resultado será "trémulo".

Dispositivo de reprodución: directiva de hosts, configuración para o propio xogo e pre_tarefas, tarefas, roles, seccións post_tarefas. Os restantes parámetros de xogo non son importantes para nós agora.

A orde das súas seccións con tarefas e roles: pre_tasks, roles, tasks, post_tasks. Xa que semanticamente a orde de execución está entre tasks и roles non está claro, entón as mellores prácticas din que estamos engadindo unha sección tasks, só se non roles... Se hai roles, entón todas as tarefas adxuntas colócanse en seccións pre_tasks/post_tasks.

O único que queda é que todo está semánticamente claro: primeiro pre_tasksentón rolesentón post_tasks.

Pero aínda non respondemos á pregunta: onde está a convocatoria do módulo? foo escribir? Necesitamos escribir un rol completo para cada módulo? Ou é mellor ter un papel groso para todo? E se non é un papel, onde debo escribir: antes ou no post?

Se non hai unha resposta razoada a estas preguntas, isto é un sinal de falta de intuición, é dicir, eses mesmos "fundamentos inestables". Imos descubrir. En primeiro lugar, unha pregunta de seguridade: se xogar ten pre_tasks и post_tasks (e non hai tarefas nin roles), entón algo pode romper se realizo a primeira tarefa desde post_tasks Vou mover ata o final pre_tasks?

Por suposto, a redacción da pregunta indica que se romperá. Pero que exactamente?

... Manipuladores. A lectura dos conceptos básicos revela un feito importante: todos os controladores son eliminados automaticamente despois de cada sección. Eses. todas as tarefas de pre_tasks, a continuación, todos os controladores que foron notificados. Despois execútanse todos os roles e todos os controladores que foron notificados nos roles. Despois post_tasks e os seus encargados.

Así, se arrastras unha tarefa desde post_tasks в pre_tasks, entón potencialmente executarao antes de que se execute o controlador. por exemplo, se está en pre_tasks o servidor web está instalado e configurado, e post_tasks se lle envía algo e, a continuación, transfire esta tarefa á sección pre_tasks levará ao feito de que no momento de "enviar" o servidor aínda non estará funcionando e todo se romperá.

Agora pensemos de novo, por que necesitamos pre_tasks и post_tasks? Por exemplo, para completar todo o necesario (incluídos os controladores) antes de cumprir o papel. A post_tasks permitiranos traballar cos resultados da execución de roles (incluídos os controladores).

Un experto astuto de Ansible diranos de que se trata. meta: flush_handlers, pero por que necesitamos flush_handlers se podemos confiar na orde de execución das seccións en xogo? Ademais, o uso de meta: flush_handlers pode darnos cousas inesperadas con controladores duplicados, dándonos avisos estraños cando se usan when у block etc. Canto mellor coñezas o ansible, máis matices podes nomear para unha solución "complicada". E unha solución sinxela -utilizando unha división natural entre pre/roles/post- non provoca matices.

E, de volta ao noso 'foo'. Onde debo poñelo? En pre, post ou roles? Obviamente, isto depende de se necesitamos os resultados do controlador para foo. Se non están alí, entón foo non necesita ser colocado nin en pre nin post - estas seccións teñen un significado especial - executando tarefas antes e despois do corpo principal do código.

Agora a resposta á pregunta "papel ou tarefa" redúcese ao que xa está en xogo: se hai tarefas alí, cómpre engadilas ás tarefas. Se hai roles, cómpre crear un rol (mesmo a partir dunha tarefa). Permíteme lembrar que as tarefas e os roles non se usan ao mesmo tempo.

Comprender os conceptos básicos de Ansible proporciona respostas razoables a preguntas aparentemente de gusto.

Tarefas e roles (segunda parte)

Agora imos discutir a situación cando estás comezando a escribir un libro de xogos. Necesitas facer foo, bar e baz. Son estas tres tarefas, un rol ou tres papeis? Para resumir a pregunta: en que momento deberías comezar a escribir papeis? Para que serve escribir roles cando podes escribir tarefas?... Que é un rol?

Un dos maiores erros (xa falei disto) é pensar que un papel é como unha función na biblioteca dun programa. Como é a descrición dunha función xenérica? Acepta argumentos de entrada, interactúa con causas secundarias, fai efectos secundarios e devolve un valor.

Agora, atención. Que se pode facer con isto no papel? Sempre é benvido a chamar efectos secundarios, esta é a esencia de todo Ansible - para crear efectos secundarios. Teñen causas secundarias? Elemental. Pero con "pase un valor e devólveo" - é aí onde non funciona. En primeiro lugar, non se pode pasar un valor a un rol. Podes establecer unha variable global cun tamaño de xogo de por vida na sección vars para o rol. Podes establecer unha variable global con toda unha vida en xogo dentro do rol. Ou mesmo coa vida útil dos libros de xogos (set_fact/register). Pero non podes ter "variables locais". Non podes "tomar un valor" e "devolvelo".

O principal disto segue: non podes escribir algo en Ansible sen causar efectos secundarios. Cambiar variables globais é sempre un efecto secundario para unha función. En Rust, por exemplo, cambiar unha variable global é unsafe. E en Ansible é o único método para influír nos valores dun papel. Teña en conta as palabras utilizadas: non "pasar un valor ao rol", senón "cambiar os valores que usa o rol". Non hai illamento entre roles. Non hai illamento entre tarefas e roles.

Total: un papel non é unha función.

Que ten de bo o papel? En primeiro lugar, o rol ten valores predeterminados (/default/main.yaml), en segundo lugar, o rol ten directorios adicionais para almacenar ficheiros.

Cales son os beneficios dos valores predeterminados? Porque na pirámide de Maslow, a táboa bastante distorsionada de prioridades variables de Ansible, os valores predeterminados dos roles son os de prioridade máis baixa (menos os parámetros da liña de comandos de Ansible). Isto significa que se precisa proporcionar valores predeterminados e non se preocupar de que anulen os valores do inventario ou das variables do grupo, os valores predeterminados dos roles son o único lugar adecuado para vostede. (Estou mentindo un pouco, hai máis |d(your_default_here), pero se falamos de lugares estacionarios, só os roles predeterminados).

Que máis hai de xenial nos papeis? Porque teñen os seus propios catálogos. Estes son directorios para variables, tanto constantes (é dicir, calculadas para o rol) como dinámicas (hai un patrón ou un antipatrón - include_vars xunto con {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Estes son os directorios para files/, templates/. Ademais, permítelle ter os seus propios módulos e complementos (library/). Pero, en comparación coas tarefas dun libro de xogos (que tamén pode ter todo isto), o único beneficio aquí é que os ficheiros non se botan nunha pila, senón en varias pilas separadas.

Un detalle máis: podes tentar crear roles que estarán dispoñibles para a súa reutilización (vía galaxia). Coa chegada das coleccións, a distribución de roles pódese considerar case esquecida.

Así, os roles teñen dúas características importantes: teñen valores predeterminados (unha característica única) e permítenche estruturar o teu código.

Volvendo á pregunta orixinal: cando facer tarefas e cando facer papeis? As tarefas dun libro de xogos úsanse con máis frecuencia como "pegamento" antes/despois de papeis ou como elemento de construción independente (non debería haber papeis no código). Unha morea de tarefas normais mesturadas con papeis é un desleixo inequívoco. Debe adherirse a un estilo específico: unha tarefa ou un papel. Os roles proporcionan separación de entidades e valores predeterminados, as tarefas permítenche ler o código máis rápido. Normalmente, o código máis "estacionario" (importante e complexo) ponse en roles e os guións auxiliares escríbense en estilo de tarefa.

É posible facer import_role como unha tarefa, pero se escribes isto, prepárate para explicar ao teu propio sentido da beleza por que queres facelo.

Un lector astuto pode dicir que os roles poden importar roles, os roles poden ter dependencias a través de galaxy.yml, e tamén hai unha terrible e terrible include_role — Lémbrovos que estamos a mellorar as habilidades en Ansible básico, e non en ximnasia artística.

Manexadores e tarefas

Comentemos outra cousa obvia: os manipuladores. Saber utilizalos correctamente é case unha arte. Cal é a diferenza entre un manipulador e un arrastre?

Xa que estamos lembrando o básico, aquí tes un exemplo:

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

Os controladores do rol están situados en rolename/handlers/main.yaml. Os xestores rebuscan entre todos os participantes da xogada: pre/post_tasks poden tirar xestores de roles e un rol pode sacar xestores da xogada. Non obstante, as chamadas de "papel cruzado" aos controladores causan moito máis peso que repetir un manejador trivial. (Outro elemento das mellores prácticas é tentar non repetir os nomes dos controladores).

A principal diferenza é que a tarefa sempre se executa (idempotentemente) (etiquetas máis/menos e when), e o controlador - por cambio de estado (notifique os incendios só se se cambiou). Que significa isto? Por exemplo, o feito de que cando se reinicie, se non houbo ningún cambio, non haberá controlador. Por que pode ser que necesitemos executar o controlador cando non houbo ningún cambio na tarefa de xeración? Por exemplo, porque algo rompeu e cambiou, pero a execución non chegou ao manejador. Por exemplo, porque a rede estivo temporalmente inactiva. A configuración cambiou, o servizo non se reiniciou. A próxima vez que o inicie, a configuración xa non cambia e o servizo permanece coa versión antiga da configuración.

Non se pode resolver a situación coa configuración (máis precisamente, podes inventar un protocolo de reinicio especial con marcas de ficheiros, etc., pero isto xa non é "ansible básico" en calquera forma). Pero hai outra historia común: instalamos a aplicación, gravámola .service-arquivo, e agora queremos daemon_reload и state=started. E o lugar natural para isto parece ser o manejador. Pero se o convertes non nun manejador senón nunha tarefa ao final dunha lista de tarefas ou rol, entón executarase de forma idempotente cada vez. Aínda que o libro de xogos rompese polo medio. Isto non resolve en absoluto o problema reiniciado (non se pode facer unha tarefa co atributo reiniciado, porque se perde a idempotencia), pero definitivamente paga a pena facer state=started, a estabilidade xeral dos playbooks aumenta, porque o número de conexións e o estado dinámico diminúe.

Outra propiedade positiva do manejador é que non obstruye a saída. Non houbo cambios - non se omitiron nin se ok na saída - máis fácil de ler. Tamén é unha propiedade negativa: se atopa un erro tipográfico nunha tarefa executada linealmente na primeira execución, os controladores executaranse só cando se modifiquen, é dicir. nalgunhas condicións - moi raramente. Por exemplo, por primeira vez na miña vida cinco anos despois. E, por suposto, haberá un erro de tipografía no nome e romperase todo. E se non os executas a segunda vez, non hai cambios.

Separadamente, temos que falar da dispoñibilidade de variables. Por exemplo, se notificas unha tarefa cun bucle, que haberá nas variables? Podes adiviñar analíticamente, pero non sempre é trivial, especialmente se as variables veñen de diferentes lugares.

... Polo tanto, os manipuladores son moito menos útiles e moito máis problemáticos do que parecen. Se podes escribir algo fermoso (sen adornos) sen manipuladores, é mellor facelo sen eles. Se non funciona moi ben, é mellor con eles.

O lector corrosivo sinala con razón que non falamos listenque un xestor pode chamar a notificación para outro xestor, que un xestor pode incluír import_tasks (que pode facer include_role con with_items), que o sistema de xestor en Ansible é Turing-completo, que os xestores de include_role se cruzan dun xeito curioso cos controladores do xogo, etc. .d. - todo isto claramente non é o "básico").

Aínda que hai un WTF específico que en realidade é unha característica que debes ter en conta. Se a súa tarefa é executada con delegate_to e ten notificar, entón o controlador correspondente execútase sen delegate_to, é dicir. no host onde se asigna o xogo. (Aínda que o xestor, por suposto, pode ter delegate_to Igual).

Por separado, quero dicir algunhas palabras sobre os papeis reutilizables. Antes de que aparecesen as coleccións, había a idea de que podías facer papeis universais que poderían ser ansible-galaxy install e foi. Funciona en todos os sistemas operativos de todas as variantes en todas as situacións. Entón, a miña opinión: non funciona. Calquera papel con masa include_vars, que admite 100500 casos, está condenado a un abismo de erros de esquina. Poden cubrirse con probas masivas, pero como con calquera proba, ou tes un produto cartesiano de valores de entrada e unha función total, ou tes "escenarios individuais cubertos". A miña opinión é que é moito mellor se o papel é lineal (complexidade ciclomática 1).

Menos ses (explícitos ou declarativos - no formulario when ou forma include_vars por conxunto de variables), mellor será o papel. Ás veces hai que facer pólas, pero, repito, cantos menos haxa, mellor. Entón, parece un bo papel con galaxia (funciona!) cunha chea de when pode ser menos preferible que o papel "propio" de cinco tarefas. O momento no que o papel con galaxia é mellor é cando comezas a escribir algo. O momento no que empeora é cando algo rompe e tes a sospeita de que é polo "papel coa galaxia". Ábreo e hai cinco inclusións, oito follas de tarefas e unha pila when'ov... E temos que descubrir isto. En lugar de 5 tarefas, unha lista lineal na que non hai nada que romper.

Nas seguintes partes

  • Un pouco sobre o inventario, as variables do grupo, o complemento host_group_vars e os hostvars. Como facer un nó gordiano con espaguetes. Variables de alcance e precedencia, modelo de memoria Ansible. "Entón, onde almacenamos o nome de usuario para a base de datos?"
  • jinja: {{ jinja }} - plastilina suave nosql notype nosense. Está en todas partes, incluso onde non o esperas. Un pouco sobre !!unsafe e delicioso yaml.

Fonte: www.habr.com

Engadir un comentario