Noções básicas do Ansible, sem as quais seus manuais serão um pedaço de massa pegajosa

Eu faço muitas revisões do código Ansible de outras pessoas e escrevo muito. Ao analisar erros (de outras pessoas e meus), bem como de várias entrevistas, percebi o principal erro que os usuários do Ansible cometem - eles se envolvem em coisas complexas sem dominar os básicos.

Para corrigir esta injustiça universal, decidi escrever uma introdução ao Ansible para quem já o conhece. Já aviso, isso não é uma releitura de homem, é uma leitura longa com muitas letras e sem fotos.

O nível esperado do leitor é que vários milhares de linhas de yamla já tenham sido escritas, algo já esteja em produção, mas “de alguma forma tudo está torto”.

Títulos

O principal erro que um usuário Ansible comete é não saber como algo é chamado. Se você não sabe os nomes, não consegue entender o que diz a documentação. Um exemplo vivo: durante uma entrevista, uma pessoa que parecia dizer que escrevia muito em Ansible não conseguiu responder à pergunta “em que elementos consiste um playbook?” E quando sugeri que “era esperada a resposta de que o manual consiste em brincadeira”, seguiu-se o comentário condenatório “não usamos isso”. As pessoas escrevem Ansible por dinheiro e não usam o jogo. Eles realmente usam, mas não sabem o que é.

Então vamos começar com algo simples: como se chama. Talvez você saiba disso, ou talvez não, porque não prestou atenção ao ler a documentação.

ansible-playbook executa o playbook. Um playbook é um arquivo com a extensão yml/yaml, dentro do qual existe algo assim:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Já percebemos que todo esse arquivo é um manual. Podemos mostrar onde estão as funções e onde estão as tarefas. Mas onde está a brincadeira? E qual é a diferença entre jogo e papel ou manual?

Está tudo na documentação. E eles sentem falta disso. Iniciantes - porque é demais e você não vai lembrar de tudo de uma vez. Experiente - porque “coisas triviais”. Se você tiver experiência, releia essas páginas pelo menos uma vez a cada seis meses e seu código se tornará líder de classe.

Então, lembre-se: Playbook é uma lista que consiste em brincadeiras e import_playbook.
Esta é uma peça:

- hosts: group1
  roles:
    - role1

e esta também é outra peça:

- hosts: group2,group3
  tasks:
    - debug:

O que é brincar? Por que ela está?

Play é um elemento-chave para um playbook, porque play e somente play associa uma lista de funções e/ou tarefas a uma lista de hosts nos quais elas devem ser executadas. Nas profundezas da documentação você pode encontrar menção a delegate_to, plug-ins de pesquisa local, configurações específicas de CLI de rede, hosts de salto, etc. Eles permitem alterar ligeiramente o local onde as tarefas são executadas. Mas, esqueça isso. Cada uma dessas opções inteligentes tem usos muito específicos e definitivamente não são universais. E estamos falando de coisas básicas que todos deveriam conhecer e usar.

Se você quiser apresentar “alguma coisa” “em algum lugar”, você escreve uma peça. Não é um papel. Não é uma função com módulos e delegados. Você pega e escreve a peça. Nele, no campo hosts você lista onde executar, e em funções/tarefas - o que executar.

Simples, certo? Como poderia ser de outra forma?

Um dos momentos característicos em que as pessoas desejam fazer isso não através da brincadeira é o “papel que configura tudo”. Gostaria de ter uma função que configurasse tanto servidores do primeiro tipo quanto servidores do segundo tipo.

Um exemplo arquetípico é o monitoramento. Eu gostaria de ter uma função de monitoramento que configure o monitoramento. A função de monitoramento é atribuída aos hosts de monitoramento (de acordo com o jogo). Mas acontece que para monitorar precisamos entregar pacotes aos hosts que estamos monitorando. Por que não usar delegado? Você também precisa configurar o iptables. delegar? Você também precisa escrever/corrigir uma configuração para o DBMS para habilitar o monitoramento. delegar! E se falta criatividade, então você pode fazer uma delegação include_role em um loop aninhado usando um filtro complicado em uma lista de grupos, e dentro include_role você pode fazer mais delegate_to de novo. E lá vamos nós...

Um bom desejo - ter um único papel de monitorização, que “faça tudo” - leva-nos a um inferno completo, do qual na maioria das vezes só há uma saída: reescrever tudo do zero.

Onde o erro aconteceu aqui? No momento em que você descobriu que para fazer a tarefa "x" no host X você tinha que ir até o host Y e fazer "y" lá, você tinha que fazer um exercício simples: ir e escrever play, que no host Y faz y. Não adicione algo a "x", mas escreva do zero. Mesmo com variáveis ​​codificadas.

Parece que tudo nos parágrafos acima foi dito corretamente. Mas este não é o seu caso! Porque você deseja escrever código reutilizável que seja DRY e semelhante a uma biblioteca, e precisa procurar um método para fazer isso.

É aqui que se esconde outro erro grave. Um erro que transformou muitos projetos de escritos toleravelmente (poderia ser melhor, mas tudo funciona e é fácil de terminar) em um horror completo que nem o autor consegue entender. Funciona, mas Deus não permita que você mude alguma coisa.

O erro é: role é uma função de biblioteca. Esta analogia arruinou tantos bons começos que é simplesmente triste de assistir. A função não é uma função de biblioteca. Ela não consegue fazer cálculos e não consegue tomar decisões no nível do jogo. Lembre-me quais decisões o jogo toma?

Obrigado, você está certo. O jogo toma uma decisão (mais precisamente, contém informações) sobre quais tarefas e funções executar em quais hosts.

Se você delegar essa decisão a uma função, e mesmo com cálculos, você se condenará (e aquele que tentará analisar seu código) a uma existência miserável. A função não decide onde será desempenhada. Esta decisão é tomada por jogo. O papel faz o que é dito, onde é dito.

Por que é perigoso programar em Ansible e por que COBOL é melhor que Ansible falaremos no capítulo sobre variáveis ​​e jinja. Por enquanto, digamos uma coisa: cada um de seus cálculos deixa um rastro indelével de mudanças nas variáveis ​​​​globais e você não pode fazer nada a respeito. Assim que os dois “traços” se cruzaram, tudo desapareceu.

Nota para os mais sensíveis: a função certamente pode influenciar o fluxo de controle. Comer delegate_to e tem usos razoáveis. Comer meta: end host/play. Mas! Lembra que ensinamos o básico? Esqueci sobre delegate_to. Estamos falando do código Ansible mais simples e bonito. Que é fácil de ler, fácil de escrever, fácil de depurar, fácil de testar e fácil de concluir. Então, mais uma vez:

play e somente play decide em quais hosts o que será executado.

Nesta seção, tratamos da oposição entre jogo e papel. Agora vamos falar sobre a relação entre tarefas e funções.

Tarefas e Funções

Considere brincar:

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

Digamos que você precise fazer foo. E parece foo: name=foobar state=present. Onde devo escrever isso? no pré? publicar? Criar uma função?

...E para onde foram as tarefas?

Estamos começando com o básico novamente – o dispositivo de jogo. Se você flutuar nessa questão, não poderá usar o jogo como base para todo o resto, e seu resultado será "instável".

Dispositivo de jogo: diretiva de hosts, configurações para o próprio jogo e seções pré-tarefas, tarefas, funções, pós-tarefas. Os demais parâmetros de jogo não são importantes para nós agora.

A ordem de suas seções com tarefas e funções: pre_tasks, roles, tasks, post_tasks. Como semanticamente a ordem de execução está entre tasks и roles não está claro, então as práticas recomendadas dizem que estamos adicionando uma seção tasks, somente se não roles. Se houver roles, então todas as tarefas anexadas são colocadas em seções pre_tasks/post_tasks.

Resta apenas que tudo esteja semanticamente claro: primeiro pre_tasksentão rolesentão post_tasks.

Mas ainda não respondemos à pergunta: onde está a chamada do módulo? foo escrever? Precisamos escrever uma função inteira para cada módulo? Ou é melhor ter um papel grosso para tudo? E se não for um papel, onde devo escrever - antes ou depois?

Se não houver uma resposta fundamentada para essas perguntas, isso é um sinal de falta de intuição, ou seja, desses mesmos “fundamentos instáveis”. Vamos descobrir. Primeiro, uma questão de segurança: se o jogo tiver pre_tasks и post_tasks (e não há tarefas ou funções), então algo pode falhar se eu executar a primeira tarefa de post_tasks Vou movê-lo para o fim pre_tasks?

É claro que o texto da pergunta sugere que ela irá quebrar. Mas o que exatamente?

... Manipuladores. A leitura do básico revela um fato importante: todos os manipuladores são liberados automaticamente após cada seção. Aqueles. todas as tarefas de pre_tasks, então todos os manipuladores que foram notificados. Em seguida, todas as funções e todos os manipuladores que foram notificados nas funções são executados. Depois post_tasks e seus manipuladores.

Assim, se você arrastar uma tarefa do post_tasks в pre_tasks, então potencialmente você o executará antes que o manipulador seja executado. por exemplo, se em pre_tasks o servidor web está instalado e configurado, e post_tasks algo é enviado para ele, então transfira esta tarefa para a seção pre_tasks levará ao fato de que no momento do “envio” o servidor ainda não estará funcionando e tudo quebrará.

Agora vamos pensar novamente, por que precisamos pre_tasks и post_tasks? Por exemplo, para completar tudo o que é necessário (incluindo manipuladores) antes de cumprir a função. A post_tasks nos permitirá trabalhar com os resultados da execução de funções (incluindo manipuladores).

Um especialista astuto do Ansible nos dirá o que é. meta: flush_handlers, mas por que precisamos de flush_handlers se podemos confiar na ordem de execução das seções em jogo? Além disso, o uso de meta: flush_handlers pode nos dar coisas inesperadas com manipuladores duplicados, dando-nos avisos estranhos quando usados when у block etc. Quanto melhor você conhecer o ansible, mais nuances poderá nomear para uma solução “complicada”. E uma solução simples - usar uma divisão natural entre pré/funções/pós - não causa nuances.

E, de volta ao nosso 'foo'. Onde devo colocá-lo? Em pré, pós ou funções? Obviamente, isso depende se precisamos dos resultados do manipulador para foo. Se eles não estiverem lá, então foo não precisa ser colocado em pré ou pós - essas seções têm um significado especial - executando tarefas antes e depois do corpo principal do código.

Agora a resposta à pergunta “função ou tarefa” se resume ao que já está em jogo - se houver tarefas lá, você precisará adicioná-las às tarefas. Se houver funções, você precisará criar uma função (mesmo que seja de uma tarefa). Deixe-me lembrá-lo de que tarefas e funções não são usadas ao mesmo tempo.

Compreender os fundamentos do Ansible fornece respostas razoáveis ​​para questões aparentemente de gosto.

Tarefas e funções (parte dois)

Agora vamos discutir a situação quando você está apenas começando a escrever um manual. Você precisa fazer foo, bar e baz. Essas três tarefas são uma função ou três funções? Para resumir a questão: em que ponto você deve começar a escrever papéis? Qual é o sentido de escrever papéis quando você pode escrever tarefas?... O que é um papel?

Um dos maiores erros (já falei sobre isso) é pensar que um papel é como uma função na biblioteca de um programa. Qual é a aparência de uma descrição genérica de função? Aceita argumentos de entrada, interage com causas secundárias, causa efeitos colaterais e retorna um valor.

Agora, atenção. O que pode ser feito a partir disso na função? Você é sempre bem-vindo para chamar efeitos colaterais, esta é a essência de todo o Ansible - criar efeitos colaterais. Tem causas colaterais? Elementar. Mas com “passar um valor e devolvê-lo” – é aí que não funciona. Primeiro, você não pode passar um valor para uma função. Você pode definir uma variável global com um tamanho vitalício de jogo na seção vars da função. Você pode definir uma variável global com um tempo de vida em jogo dentro da função. Ou mesmo com a vida útil dos manuais (set_fact/register). Mas você não pode ter "variáveis ​​locais". Você não pode “pegar um valor” e “devolvê-lo”.

O principal é que você não pode escrever algo no Ansible sem causar efeitos colaterais. Alterar variáveis ​​globais é sempre um efeito colateral para uma função. Em Rust, por exemplo, alterar uma variável global é unsafe. E no Ansible é o único método para influenciar os valores de uma função. Observe as palavras utilizadas: não “passar um valor para a função”, mas “alterar os valores que a função utiliza”. Não há isolamento entre funções. Não há isolamento entre tarefas e funções.

Total: um papel não é uma função.

O que há de bom no papel? Primeiro, a função possui valores padrão (/default/main.yaml), em segundo lugar, a função possui diretórios adicionais para armazenar arquivos.

Quais são os benefícios dos valores padrão? Porque na pirâmide de Maslow, a tabela bastante distorcida de prioridades variáveis ​​do Ansible, os padrões de função são os de prioridade mais baixa (menos os parâmetros de linha de comando do Ansible). Isso significa que se você precisar fornecer valores padrão e não se preocupar com a possibilidade de eles substituirem os valores do inventário ou das variáveis ​​de grupo, os padrões de função são o único lugar certo para você. (Estou mentindo um pouco - há mais |d(your_default_here), mas se falamos de locais fixos, então apenas padrões de função).

O que mais há de bom nos papéis? Porque eles têm seus próprios catálogos. Estes são diretórios para variáveis, tanto constantes (ou seja, calculadas para a função) quanto dinâmicas (existe um padrão ou um antipadrão - include_vars com {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Estes são os diretórios para files/, templates/. Além disso, permite que você tenha seus próprios módulos e plugins (library/). Mas, em comparação com as tarefas de um manual (que também pode ter tudo isso), o único benefício aqui é que os arquivos não são despejados em uma pilha, mas em várias pilhas separadas.

Mais um detalhe: você pode tentar criar funções que estarão disponíveis para reutilização (via galaxy). Com o advento das coleções, a distribuição de funções pode ser considerada quase esquecida.

Assim, as funções possuem dois recursos importantes: possuem padrões (um recurso exclusivo) e permitem estruturar seu código.

Voltando à questão original: quando fazer tarefas e quando fazer papéis? As tarefas em um manual são mais frequentemente usadas como “cola” antes/depois das funções ou como um elemento de construção independente (então não deve haver funções no código). Uma pilha de tarefas normais misturadas com funções é um desleixo inequívoco. Você deve aderir a um estilo específico - seja uma tarefa ou uma função. As funções fornecem separação de entidades e padrões, as tarefas permitem que você leia o código mais rapidamente. Normalmente, códigos mais “estacionários” (importantes e complexos) são colocados em funções e scripts auxiliares são escritos em estilo de tarefa.

É possível fazer import_role como uma tarefa, mas se você escrever isso, esteja preparado para explicar ao seu próprio senso de beleza por que deseja fazer isso.

Um leitor astuto pode dizer que as funções podem importar funções, as funções podem ter dependências via galaxy.yml, e também há uma terrível e terrível include_role — Lembro que estamos aprimorando as habilidades no Ansible básico, e não na ginástica artística.

Manipuladores e tarefas

Vamos discutir outra coisa óbvia: manipuladores. Saber utilizá-los corretamente é quase uma arte. Qual é a diferença entre um manipulador e um arrasto?

Já que estamos lembrando o básico, aqui está um exemplo:

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

Os manipuladores da função estão localizados em rolename/handlers/main.yaml. Os manipuladores vasculham todos os participantes da peça: pre/post_tasks podem extrair manipuladores de função e uma função pode extrair manipuladores da peça. No entanto, chamadas de "funções cruzadas" para manipuladores causam muito mais problemas do que repetir um manipulador trivial. (Outro elemento das melhores práticas é tentar não repetir nomes de manipuladores).

A principal diferença é que a tarefa é sempre executada (idempotentemente) (tags mais/menos e when) e o manipulador - por mudança de estado (notificar os incêndios somente se tiver sido alterado). O que isto significa? Por exemplo, o fato de que quando você reiniciar, se não houver alteração, não haverá manipulador. Por que precisamos executar o manipulador quando não houve alteração na tarefa geradora? Por exemplo, porque algo quebrou e mudou, mas a execução não chegou ao manipulador. Por exemplo, porque a rede estava temporariamente inativa. A configuração foi alterada, o serviço não foi reiniciado. Na próxima vez que você iniciá-lo, a configuração não será mais alterada e o serviço permanecerá com a versão antiga da configuração.

A situação com a configuração não pode ser resolvida (mais precisamente, você pode inventar um protocolo de reinicialização especial para si mesmo com sinalizadores de arquivo, etc., mas isso não é mais 'ansible básico' de qualquer forma). Mas há outra história comum: instalamos o aplicativo, gravamos .service-file, e agora queremos daemon_reload и state=started. E o lugar natural para isso parece ser o manipulador. Mas se você não torná-lo um manipulador, mas uma tarefa no final de uma lista de tarefas ou função, ela será executada de forma idempotente todas as vezes. Mesmo que o manual tenha quebrado no meio. Isso não resolve de forma alguma o problema de reinicialização (você não pode executar uma tarefa com o atributo reiniciado, porque a idempotência é perdida), mas definitivamente vale a pena fazer state=started, a estabilidade geral dos playbooks aumenta, porque o número de conexões e o estado dinâmico diminuem.

Outra propriedade positiva do manipulador é que ele não obstrui a saída. Não houve alterações - nenhum extra foi ignorado ou ok na saída - mais fácil de ler. Também é uma propriedade negativa - se você encontrar um erro de digitação em uma tarefa executada linearmente na primeira execução, os manipuladores serão executados somente quando alterados, ou seja, sob algumas condições - muito raramente. Por exemplo, pela primeira vez na minha vida, cinco anos depois. E, claro, haverá um erro de digitação no nome e tudo quebrará. E se você não executá-los pela segunda vez, não haverá alteração.

Separadamente, precisamos falar sobre a disponibilidade de variáveis. Por exemplo, se você notificar uma tarefa com um loop, o que estará nas variáveis? Você pode adivinhar analiticamente, mas nem sempre é trivial, especialmente se as variáveis ​​vierem de lugares diferentes.

... Portanto, os manipuladores são muito menos úteis e muito mais problemáticos do que parecem. Se você consegue escrever algo lindamente (sem frescuras) sem manipuladores, é melhor fazê-lo sem eles. Se não funcionar lindamente, é melhor com eles.

O leitor corrosivo aponta, com razão, que não discutimos listenque um manipulador pode chamar notify para outro manipulador, que um manipulador pode incluir import_tasks (que pode fazer include_role com with_items), que o sistema de manipuladores em Ansible é Turing-complete, que manipuladores de include_role se cruzam de maneira curiosa com manipuladores de play, etc. - tudo isso claramente não é o “básico”).

Embora exista um WTF específico que é, na verdade, um recurso que você precisa ter em mente. Se sua tarefa for executada com delegate_to e tem notificação, então o manipulador correspondente é executado sem delegate_to, ou seja no host onde o jogo é atribuído. (Embora o manipulador, é claro, possa ter delegate_to também).

Separadamente, quero dizer algumas palavras sobre funções reutilizáveis. Antes do surgimento das coleções, havia uma ideia de que era possível criar papéis universais que poderiam ser ansible-galaxy install e foi. Funciona em todos os sistemas operacionais de todas as variantes em todas as situações. Então, minha opinião: não funciona. Qualquer papel com massa include_vars, suportando 100500 casos, está fadado ao abismo de bugs de casos extremos. Eles podem ser cobertos por testes massivos, mas como acontece com qualquer teste, ou você tem um produto cartesiano de valores de entrada e uma função total ou tem “cenários individuais cobertos”. Minha opinião é que é muito melhor se o papel for linear (complexidade ciclomática 1).

Quanto menos ifs (explícitos ou declarativos - na forma when ou formulário include_vars por conjunto de variáveis), melhor será o papel. Às vezes é preciso fazer galhos, mas, repito, quanto menos, melhor. Então parece um bom papel para o Galaxy (funciona!) com um monte de when pode ser menos preferível do que o papel “próprio” de cinco tarefas. O momento em que o papel do Galaxy é melhor é quando você começa a escrever algo. O momento em que piora é quando algo quebra e você suspeita que seja por causa do “papel com a galáxia”. Você abre e há cinco inclusões, oito folhas de tarefas e uma pilha when'ov... E precisamos descobrir isso. Em vez de 5 tarefas, uma lista linear na qual não há nada para quebrar.

Nas seguintes partes

  • Um pouco sobre inventário, variáveis ​​de grupo, plugin host_group_vars, hostvars. Como dar nó górdio com espaguete. Variáveis ​​de escopo e precedência, modelo de memória Ansible. “Então, onde armazenamos o nome de usuário do banco de dados?”
  • jinja: {{ jinja }} — nosql notype nosense plasticina macia. Está em todo lugar, mesmo onde você não espera. Um pouco sobre !!unsafe e delicioso yaml.

Fonte: habr.com

Adicionar um comentário