Eu faço muitas análises de código de terceiros no Ansible e também escrevo bastante. Durante a análise de erros (tanto os meus quanto os de outros), além de algumas entrevistas, percebi o principal erro que os usuários do Ansible cometem — eles se aventuram em complexidades sem dominar o básico.
Para corrigir essa injustiça universal, decidi escrever uma introdução ao Ansible para aqueles que já têm alguma familiaridade. Aviso que isso não é um resumo de manuais, é um longo texto cheio de letras e sem imagens.
O nível esperado do leitor — já ter escrito algumas milhares de linhas de YAML, já ter algo em produção, mas "tudo parece meio torto".
Nomes
O principal erro do usuário do Ansible é não saber como as coisas se chamam. Se você não conhece os nomes, não pode entender o que está escrito na documentação. Um exemplo concreto: numa entrevista, uma pessoa, que aparentemente afirmava ter escrito muito em Ansible, não conseguiu responder à pergunta "quais elementos compõem um playbook?". E quando eu sugeri que "esperava-se que a resposta fosse que playbook consiste de play", o comentário devastador foi "nós não usamos isso". As pessoas escrevem em Ansible por dinheiro e não utilizam play. Na verdade, elas usam, mas não sabem o que é.
Então, vamos começar com o básico: como as coisas são chamadas. Você pode já saber disso, ou pode não saber, porque não prestou atenção ao ler a documentação.
ansible-playbook executa playbooks. Playbook — é um arquivo com a extensão yml/yaml, dentro do qual tem algo assim:
---
- hosts: group1
roles:
- role1
- hosts: group2,group3
tasks:
- debug:Já entendemos que todo esse arquivo é um playbook. Podemos mostrar onde estão as roles, onde estão as tasks. Mas onde está o play aqui? E qual a diferença entre play, role ou playbook?
Isso está tudo na documentação. E isso é ignorado. Iniciantes — porque tem informação demais e você não consegue memorizar tudo de uma vez. Os experientes — porque "são coisas triviais". Se você é experiente — releia essas páginas ao menos uma vez a cada seis meses, e seu código ficará muito melhor.
E assim, memorizem: Playbook — é uma lista que consiste em play e import_playbook.
Essa é uma play:
- hosts: group1
roles:
- role1e essa também é outra play:
- hosts: group2,group3
tasks:
- debug:O que é play? Para que serve?
Play — é o elemento chave do playbook, porque play e apenas play conecta a lista de roles e/ou tasks à lista de hosts onde elas devem ser executadas. Nas profundezas da documentação você pode encontrar menções sobre delegate_to, plugins de lookup locais, configurações específicas de network-cli, hosts de salto, etc. Eles permitem mudar ligeiramente o local de execução das tarefas. Mas, esqueça isso. Cada uma dessas opções engenhosas tem aplicações muito específicas, e definitivamente não são universais. Estamos falando sobre as coisas básicas que todos devem saber e usar.
Se você quer "executar algo" "em algum lugar" — você escreve um play. Não um papel. Não um papel com módulos e delegados. Você pega e escreve um play. No qual, no campo hosts você lista onde executar, e em roles/tasks — o que executar.
Fácil, certo? E como poderia ser diferente?
Um dos momentos característicos quando as pessoas têm a vontade de fazer isso não através de um play, é "o papel que configura tudo". A vontade é ter um papel que configure e servidor do primeiro tipo, e servidores do segundo tipo.
Um exemplo arquetípico é a monitoração. A vontade é ter um papel de monitoração que configure a monitoração. O papel de monitoração é atribuído aos hosts de monitoração (no respectivo play). Mas, acontece que para monitorar precisamos instalar pacotes nos hosts que estamos monitorando. Por que não usar o delegate? Além disso, é necessário configurar os iptables. delegate? E ainda, é preciso escrever/corrigir a configuração do SGBD para que a monitoração funcione. delegate! E se a criatividade surgir, pode-se fazer a delegação include_role em um ciclo aninhado com um filtro engenhoso na lista de grupos, e dentro include_role pode-se ainda fazer delegate_to novamente. E assim vai…
Um desejo louvável — ter um único papel de monitoração que "faça tudo" — nos leva a um verdadeiro inferno do qual a única saída é: reescrever tudo do zero.
Onde ocorreu o erro? No momento em que você percebeu que para executar a tarefa "x" no host X você precisaria ir ao host Y e fazer "y" lá, você deveria ter feito um simples exercício: ir e escrever um play, que no host Y faça y. Não adicionar algo em "x", mas escrever do zero. Mesmo que com variáveis hardcoded.
Aparentemente, nos parágrafos acima tudo foi dito corretamente. Mas esse não é o seu caso! Porque você quer escrever código reutilizável, que seja DRY e parecido com uma biblioteca, e precisa encontrar um jeito de fazer isso.
Aqui está mais um erro grosseiro escondido. Um erro que transformou muitos projetos de textos até aceitáveis (pode ser melhor, mas tudo funciona e é fácil de adicionar) em um completo caos, onde até mesmo o autor não consegue se entender. Funciona, mas que Deus nos proteja de mudar algo.
Esse erro soa assim: a função de um papel é uma função de biblioteca. Essa analogia prejudicou tantas boas iniciativas que é triste de ver. Papel não é uma função de biblioteca. Ele não pode fazer cálculos e não pode tomar decisões em nível de play. Me lembre, quais decisões o play toma?
Obrigado, você está certo. O play toma a decisão (mais precisamente, contém a informação) sobre quais tarefas e papéis realizar em quais hosts.
Se você delegar essa decisão a um papel, e ainda por cima com cálculos, você está condenando a si mesmo (e aquele que tentar entender seu código) a uma existência lamentável. O papel não decide onde deve ser executado. Essa decisão é feita pelo play. O papel faz o que lhe foi dito, onde lhe foi dito.
Por que programar em Ansible é perigoso, e por que COBOL é melhor que Ansible, discutiremos no capítulo sobre variáveis e jinja. Por enquanto, vou dizer apenas uma coisa — cada cálculo que você realiza deixa uma marca indelével de modificação de variáveis globais, e você não pode fazer nada a respeito. Assim que duas "marcas" se cruzam — tudo se perdeu.
Observação para os mais meticulosos: o papel, sem dúvida, pode influenciar o fluxo de controle. Existe delegate_to e ele tem aplicações razoáveis. Existe meta: end host/play. Mas! Lembre-se, estamos aprendendo os fundamentos? Esqueceram sobre delegate_to. Estamos falando sobre o código mais simples e bonito em Ansible. Que é fácil de ler, fácil de escrever, fácil de depurar, fácil de testar e fácil de adicionar. Então, mais uma vez:
o play e somente o play decide em quais hosts o que é executado.
Nesta seção, discutimos a oposição entre play e papel. Agora vamos falar sobre a relação entre tarefas vs papel.
Tarefas e Papéis
Vamos considerar o play:
- hosts: somegroup
pre_tasks:
- some_tasks1:
roles:
- role1
- role2
post_tasks:
- some_task2:
- some_task3:Suponha que você precise fazer foo. E isso se parece com foo: name=foobar state=present. Onde isso deve ser escrito? em pre? post? Criar um papel?
… E onde foram parar as tarefas?
Estamos novamente começando do básico — a estrutura do play. Se você não está confortável com isso, não pode usar o play como base para tudo o mais, e seu resultado se torna "instável".
Diretiva play: hosts, configurações do próprio play e das seções pre_tasks, tasks, roles, post_tasks. Outros parâmetros para o play não são importantes agora.
A ordem das seções com tarefas e funções: pre_tasks, roles, tasks, post_tasks. Como a ordem de execução entre tasks e roles não é clara, as melhores práticas dizem que devemos adicionar a seção tasks, apenas se não houver roles. Se existir roles, todas as tarefas correspondentes são colocadas nas seções pre_tasks/post_tasks.
Resta apenas o que é semanticamente claro: primeiro pre_tasks, depois roles, depois post_tasks.
Mas ainda não respondemos à pergunta: onde devemos escrever a chamada do módulo foo ? Precisamos escrever um papel inteiro para cada módulo? Ou é melhor ter um papel abrangente para tudo? E se não for um papel, onde devemos escrever — no pre ou no post?
Se não houver uma resposta fundamentada para essas perguntas, isso indica a falta de intuição, ou seja, as "bases instáveis". Vamos entender isso. Primeiro, uma pergunta de verificação: se o play tiver pre_tasks e post_tasks (e não houver nenhuma tarefa nem função), algo pode quebrar se eu mover a primeira tarefa de post_tasks para o final? pre_tasks?
Certamente, a formulação da pergunta sugere que algo vai quebrar. Mas o que exatamente?
… Os handlers. Compreender os fundamentos revela um fato importante: todos os handlers são acionados automaticamente após cada seção. Ou seja, todas as tarefas de pre_tasks, depois todos os handlers que foram notificados. Em seguida, todas as funções e todos os handlers que foram notificados nas funções. Depois, post_tasks e seus handlers.
Assim, se você mover uma tarefa de post_tasks em pre_tasks, você potencialmente a executará antes de executar o handler. Por exemplo, se em pre_tasks algo está sendo instalado e configurado servidor web, e no post_tasks , se algo é enviado para ele, mover esta tarefa para a seção pre_tasks resultará no fato de que no momento do "envio" o servidor ainda não estará iniciado e tudo quebrará.
Agora, vamos pensar mais uma vez, por que precisamos de pre_tasks e post_tasks? Например, для того, чтобы выполнить всё нужное (включая хэндлеры) до выполнения роли. А post_tasks nos permitirá trabalhar com os resultados da execução das funções (incluindo os handlers).
Um conhecedor detalhista do Ansible dirá que existe meta: flush_handlers, mas por que precisamos de flush_handlers, se podemos confiar na ordem de execução das seções no play? Além disso, o uso de meta: flush_handlers pode nos trazer surpresas com handlers repetidos, gerar avisos estranhos em caso de uso de when u block , etc. Quanto melhor você conhece o Ansible, mais nuances você será capaz de mencionar para uma solução "inteligente". E a solução mais simples — usar uma divisão natural entre pre/roles/post — não provoca nuances.
E voltando ao nosso 'foo'. Onde colocá-lo? Em pre, post ou em roles? Obviamente, depende de precisarmos dos resultados do manipulador para foo. Se não houver, então foo não precisa ser colocado nem em pre nem em post — essas seções têm um significado especial — a execução de tarefas antes e depois do bloco de código principal.
Agora a resposta para a pergunta "papel ou tarefa" se resume ao que já existe no play — se existem tasks, então deve-se adicionar ao tasks. Se existem roles — é preciso criar um papel (mesmo que com uma única task). Lembro que tasks e roles não são usadas ao mesmo tempo.
Entender os fundamentos do Ansible fornece respostas fundamentadas para questões que, à primeira vista, parecem de gosto particular.
Tarefas e papéis (parte dois)
Agora vamos discutir a situação em que você está apenas começando a escrever um playbook. Você precisa fazer foo, bar e baz. São três tarefas, um papel ou três papéis? Resumindo a questão: em que momento devemos começar a escrever papéis? Qual é o sentido de escrever papéis quando podemos escrever tarefas?… E o que é um papel?
Um dos maiores erros (já mencionei isso) é pensar que um papel é como uma função em uma biblioteca de um programa. Como se parece uma descrição genérica de uma função? Ela recebe argumentos de entrada, interage com causas secundárias, realiza efeitos colaterais e retorna um valor.
Agora, atenção. O que disso pode ser feito em um papel? Causar efeitos colaterais — sempre que quiser, isso é a essência do Ansible — causar efeitos colaterais. Ter causas secundárias? Elementar. Mas quanto a 'passar um valor e retorná-lo' — aqui não. Primeiramente, você não pode passar um valor para um papel. Você pode definir uma variável global com um tempo de vida igual ao do play na seção vars para o papel. Você pode definir uma variável global com um tempo de vida no play dentro do papel. Ou até mesmo com um tempo de vida do playbook (set_fact/register). Mas você não pode ter 'variáveis locais'. Você não pode 'receber um valor' e 'retorná-lo'.
Dessa forma, conclui-se o principal: não se pode escrever algo em ansible e não causar efeitos colaterais. A modificação de variáveis globais é sempre um efeito colateral para uma função. No Rust, por exemplo, a alteração de uma variável global é unsafe. E no Ansible — é o único método de influenciar os valores para um papel. Note as palavras utilizadas: não é 'passar um valor para um papel', mas 'modificar os valores que o papel utiliza'. Não há isolamento entre papéis. Não há isolamento entre tarefas e papéis.
No total: um papel não é uma função.
O que há de bom em um papel? Primeiro, um papel possui valores padrão (/default/main.yaml), em segundo lugar, um papel tem diretórios adicionais para armazenar arquivos.
Quais são as vantagens dos valores padrão? O fato é que, na tabela de prioridades bastante distorcida da pirâmide de Maslow em Ansible, os valores padrão de papel — são os menos prioritários (exceto por parâmetros de linha de comando do Ansible). Isso significa que, se você precisar fornecer valores padrão e não se preocupar que eles sobrescrevam os valores do inventário ou variáveis de grupo, então os valores padrão do papel são o único lugar certo para você. (Estou mentindo um pouco — há também |d(your_default_here), mas falando sobre locais fixos — são apenas os valores padrão dos papéis).
O que mais é bom nos papéis? O fato de que eles têm seus próprios diretórios. Esses são diretórios para variáveis, tanto fixas (ou seja, calculadas para o papel), quanto dinâmicas (existe um padrão ou anti-padrão — include_vars junto com {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Esses são diretórios para files/, templates/. Além disso, permite que os papéis tenham seus próprios módulos e plugins (library/). Mas, em comparação com as tarefas em playbooks (que também podem ter tudo isso), a vantagem aqui é apenas que os arquivos não estão todos em uma única pilha, mas em várias pilhas separadas.
Outro detalhe: você pode tentar criar papéis que estarão disponíveis para reutilização (via galaxy). Após o surgimento de coleções, a disseminação de papéis pode ser considerada quase esquecida.
Assim, os papéis possuem duas características importantes: eles têm valores padrão (uma característica única) e permitem estruturar o código.
Voltando à pergunta original: quando usar tarefas e quando usar papéis? As tarefas em um playbook são frequentemente usadas tanto como "cola" antes/depois dos papéis, quanto como um elemento construtivo independente (nesse caso, o código não deve ter papéis). Uma pilha de tarefas normais misturadas com papéis é definitivamente desleixada. Deve-se aderir a um estilo específico — ou tarefas, ou papéis. Os papéis proporcionam separação de entidades e valores padrão, enquanto as tarefas permitem uma leitura mais rápida do código. Geralmente, em um papel, se destaca um código mais "fixo" (importante e complexo), e no estilo das tarefas escrevem-se scripts auxiliares.
Existe a possibilidade de fazer import_role como uma tarefa, mas se você estiver fazendo isso, esteja preparado para justificar seu senso estético sobre por que deseja fazê-lo.
Um leitor atencioso pode afirmar que funções podem importar funções, que funções podem ter dependências através do galaxy.yml, e ainda há o assustador e terrível include_role — lembre-se, estamos aprimorando habilidades básicas em Ansible, e não em ginástica rítmica.
Handlers e Tarefas
Vamos discutir mais uma coisa óbvia: handlers. Saber usá-los corretamente é quase uma arte. Qual é a diferença entre um handler e uma tarefa?
Como estamos relembrando o básico, aqui está um exemplo:
- hosts: group1
tasks:
- foo:
notify: handler1
handlers:
- name: handler1
bar:Nos roles, os handlers estão em rolename/handlers/main.yaml. Os handlers são compartilhados entre todos os participantes do play: pré/pós-tarefas podem acionar handlers de roles, e uma role pode acionar handlers de um play. No entanto, as chamadas de handlers "cross-role" causam muito mais confusão do que a repetição de um handler trivial. (Mais uma boa prática — tentar não repetir os nomes dos handlers).
A principal diferença é que uma tarefa é executada (de forma idempotente) sempre (mais ou menos com tags e when), enquanto um handler é ativado por uma mudança de estado (notify é acionado apenas se houve alteração). Quais as implicações disso? Por exemplo, o que acontece se, ao executar novamente, não houve alteração? Então, não haverá handler também. Mas por que precisaríamos executar um handler quando não houve alteração na tarefa que o gerou? Por exemplo, porque algo quebrou e houve alteração, mas a execução não chegou ao handler. Por exemplo, porque a rede estava temporariamente fora. A configuração mudou, mas o serviço não foi reiniciado. Na próxima execução, a configuração já não muda, e o serviço permanece com a versão antiga da configuração.
A situação com a configuração não é resolvível (na verdade, pode-se inventar um protocolo especial de reinício com flags de arquivo etc., mas isso já não é ‘Ansible básico’ de forma alguma). Contudo, há outra história frequente: instalamos um aplicativo, registramos seu .service- arquivo, e agora queremos daemon_reload e state=started. E o lugar natural para isso parece ser um handler. Mas se torná-lo uma tarefa no final da lista de tarefas ou um papel, ele será executado de forma idempotente toda vez. Mesmo que o playbook tenha falhado no meio. Isso não resolve o problema de restarted (não se pode fazer uma tarefa com o atributo restarted, pois a idempotência se perde), mas definitivamente vale a pena definir o state=started, a estabilidade geral dos playbooks aumenta, pois diminui o número de dependências e estados dinâmicos.
Mais uma característica positiva do handler é que ele não polui a saída. Sem alterações, não há skipped ou ok desnecessários na saída — é mais fácil de ler. Isso também é uma característica negativa — se você encontrar um erro de digitação em uma task executada linearmente na primeira execução, os handlers só serão executados quando houver changed, ou seja, em algumas condições — muito raramente. Por exemplo, a primeira vez na vida depois de cinco anos. E, naturalmente, haverá um erro de digitação no nome e tudo quebrará. E da segunda vez não será possível executá-los novamente — não há changed.
Deve-se falar separadamente sobre a acessibilidade das variáveis. Por exemplo, se você notifica uma tarefa com um loop, o que estará nas variáveis? Pode-se deduzir analiticamente, mas nem sempre isso é trivial, especialmente se as variáveis vêm de lugares diferentes.
Então, os handlers são muito menos úteis e muito mais problemáticos do que parecem. Se é possível escrever algo de forma bonita (sem complicações) sem handlers, é melhor fazê-lo sem eles. Se não é possível de forma bonita — é melhor com eles.
O leitor atento observa com razão que não discutimos listen, que um handler pode chamar notify para outro handler, que um handler pode incluir import_tasks (que pode fazer include_role com with_items), que o sistema de handlers no Ansible é Turing-completo, que os handlers de include_role se cruzam de forma curiosa com os handlers do play, etc. — tudo isso claramente não são "fundamentos".
Embora haja um WTF específico que na verdade é uma feature e que deve ser lembrado. Se você tem uma tarefa que é executada com delegate_to e ela tem notify, o handler correspondente é executado sem delegate_to, ou seja, na máquina onde o play está designado. (Embora o handler possa, naturalmente, ter delegate_to também).
Eu gostaria de dizer algumas palavras sobre roles reutilizáveis. Antes do surgimento das coleções, havia a ideia de criar roles universais que poderiam ser ansible-galaxy install e fui. Funciona em todos os sistemas operacionais, em todas as variantes, em todas as situações. Então, aqui está minha opinião: isso não funciona. Qualquer papel com suporte a 100500 casos está fadado a abismos de bugs corner case. Eles podem ser contidos com testes massivos, mas, como em qualquer teste, ou você tem o produto cartesiano dos valores de entrada e uma função total, ou você tem "cenários individuais cobertos". Minha opinião é que é muito melhor se o papel for linear (complexidade ciclomática 1). include_vars, menos ifs (explícitos ou declarativos — na forma
ou forma when por conjunto de variáveis), melhor será o papel. Às vezes é necessário fazer ramificações, mas, repito, quanto menos, melhor. Portanto, um papel que parece bom com galaxy (funciona, certo!) com um monte de include_vars pode ser menos preferível do que "seu" papel composto por cinco tarefas. O momento em que o papel com galaxy é melhor — é quando você começa a escrever algo. O momento em que ele se torna pior — é quando algo quebra e você suspeita que isso seja devido à "papel com galaxy". Você o abre e lá estão cinco inclusões, oito listas de tarefas e uma pilha when ‘s... E você precisa entender isso. Em vez de cinco tarefas em uma lista linear, na qual não há o que quebrar. whenNas próximas partes
Um pouco sobre inventário, variáveis de grupo, plugin host_group_vars, hostvars. Como amarrar o nó górdio em um emaranhado. Escopo e precedência de variáveis, modelo de memória do Ansible. "Então, onde realmente devemos armazenar o nome de usuário para o banco de dados?".
- jinja: {{ jinja }}
— nosql notype nosense, uma massa maleável. Está em todo lugar, até mesmo onde você não espera. Um pouco sobre!!unsafee yaml saboroso.Faço muitas análises de código de outras pessoas no Ansible e escrevo muito eu mesmo.
Fonte: habr.com
