Ansible: como usar --check e --diff sem erro
Entenda o que o Ansible realmente verifica com --check e --diff, por que módulos sem suporte não fazem nada e quando o dry run dá uma resposta errada.
O que o modo de verificação do Ansible faz
O modo de verificação do Ansible é uma execução de teste: ansible-playbook --check liga-se a cada host da play, pergunta a cada módulo se o estado atual já corresponde ao estado pretendido e informa o que seria alterado sem gravar nada. Adicione --diff para também mostrar o conteúdo anterior e posterior dos ficheiros que seriam alterados. Em conjunto, estas opções respondem à pergunta que deve ser feita antes de cada execução real: o que está prestes a ser alterado nestes servidores?
O modo de verificação não é uma simulação da sua playbook. Não existe qualquer modelo do servidor. Cada módulo é simplesmente instruído a consultar o estado em vez de o alterar. Um módulo que consegue responder apenas em modo de leitura informa changed e prossegue. Um módulo que não consegue responder não faz nada e não informa nada. A documentação do Ansible resume isto numa frase: "Os módulos que não suportam o modo de verificação não informam nada e não fazem nada." Essa lacuna é o motivo pelo qual uma execução de teste pode dar-lhe uma resposta errada. Por isso, a maior parte deste guia centra-se nessa lacuna.
Executar o teste: --check e --diff
ansible-playbook -i inventory.ini site.yml --check --diff --limit web1-C e -D são as formas abreviadas dos dois flags. O --limit é intencional. O diff de um host é legível. O diff de vinte hosts é algo que se percorre sem ler.
Quatro palavras de resultado representam todo o relatório.
ok: [web1]significa que a tarefa foi executada e não comunicou alterações. Para um módulo que gere estado, comoaptoutemplate, isso significa que verificou o estado e que este já corresponde ao pretendido. Paradebugouset_fact, significa apenas que a tarefa foi executada.changed: [web1]significa que o módulo prevê ter escrito alguma coisa. Com--diff, as linhas anteriores mostram o quê. A maioria dos módulos faz previsões corretas, mas alguns têm suporte parcial para o modo de verificação e avisam disso na respetiva documentação:get_urlenvia apenas um pedido HEAD no modo de verificação e informa que comunicará um estado alterado incorreto.skipping: [web1]significa que a tarefa não foi executada. O seuwhenfoi avaliado e resultou em falso, ou o módulo não pode ser executado no modo de verificação.fatal: [web1]significa que a tarefa falhou durante a verificação. Leia a mensagem antes de concluir que o playbook está avariado.
--diff imprime um diff unificado para módulos de ficheiros, com as linhas removidas marcadas com - e as linhas adicionadas marcadas com +, sob um cabeçalho cujas linhas começam por --- before e +++ after e identificam o caminho de destino. Os módulos que não escrevem ficheiros apresentam o seu próprio antes e depois; por isso, ansible.builtin.user mostra os atributos que seriam alterados, em vez do conteúdo do ficheiro.
Ative o diff permanentemente em ansible.cfg para nunca se esquecer do flag:
[diff]
always = true
context = 5Há duas verificações mais simples que devem ser feitas antes do modo de verificação. ansible-playbook site.yml --syntax-check analisa o YAML e a estrutura do play sem contactar um único host. ansible-playbook site.yml --list-tasks imprime as tarefas que seriam executadas. É assim que descobre que uma role que julgava estar etiquetada não está. Nenhuma das duas estabelece uma ligação, por isso ambas são imediatas.
O próprio modo de verificação estabelece uma ligação. Abre uma ligação SSH para todos os hosts do padrão e recolhe factos. Por isso, um host que esteja indisponível faz o teste falhar. Esse resultado já é útil por si só. É também por isso que decidir o que um playbook deve fazer com hosts inacessíveis é importante antes de incluir um teste no CI.
Por que o modo de verificação falha num servidor novo
Este play está correto. Execute-o com --check contra um servidor que ainda não tenha nginx, e a maior parte dele falhará.
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Write the site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: trueA tarefa apt informa changed, e está correta: o pacote não está instalado, portanto uma execução real iria instalá-lo. O modo de verificação não o instalou. Em seguida, a tarefa template falha, porque /etc/nginx/conf.d/ não existe neste host e nada o criou. A tarefa service também falha, porque não existe nenhuma unidade nginx para consultar. Nenhuma destas falhas é um erro no playbook. A execução de teste não tinha o estado de que precisava. É isso que a documentação quer dizer quando avisa que o modo de verificação não consegue produzir uma saída útil para uma tarefa cuja entrada depende da alteração feita por uma tarefa anterior.
Por isso, a versão correta da regra é esta: o modo de verificação é mais preciso num host onde o playbook já convergiu e produz mais ruído num host novo. Uma execução --check em que todas as tarefas informam ok é uma afirmação real sobre um host convergido, porque significa que nada seria alterado, com duas ressalvas válidas em qualquer host: uma tarefa command ignorada é um espaço em branco, não uma resposta, e um módulo com suporte parcial ao modo de verificação, como get_url, ainda pode informar incorretamente o estado de alteração. Num host completamente novo, --check indica sobretudo que o host é novo. Quando escrever o seu primeiro playbook Ansible para uma VPS, espere que a primeira execução de teste seja uma sequência de falhas e avalie o playbook pela segunda.
Por que as tarefas de comandos e shell são ignoradas no modo de verificação
ansible.builtin.command e ansible.builtin.shell não sabem o que o seu comando faz. Não existe uma forma somente de leitura de executar um binário arbitrário. Por isso, no modo de verificação, o módulo recusa-se a executá-lo. O resultado da tarefa contém skipped: true e a mensagem Command would have run if not in check mode. O resultado apresentado contém skipping: [web1].
A documentação do módulo classifica o suporte ao modo de verificação como "partial". A solução indicada é creates e removes. Defina um caminho creates para a tarefa. Assim, o modo de verificação consegue pelo menos avaliar o teste do ficheiro:
- name: Extract the release bundle
ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
args:
creates: /opt/app/bin/appSe /opt/app/bin/app já existir, o modo de verificação indica Would not run command since '/opt/app/bin/app' exists. Essa é uma resposta efetiva. Se o caminho não existir, obtém Command would have run if not in check mode. Essa também é uma resposta efetiva. Sem creates, essa tarefa não produz informação na execução de teste.
O efeito indireto é pior do que a ausência de informação. Uma tarefa command ignorada ainda regista um resultado. Esse resultado contém stdout, mas o valor é uma string vazia, porque nada foi executado. A condição da tarefa seguinte é avaliada contra essa string vazia e segue silenciosamente o ramo incorreto. Uma verificação de versão não encontra nenhuma versão, e um teste como 'active' in svc.stdout resulta em falso. O playbook produz uma resposta numa execução real e outra no modo de teste, sem apresentar um erro que permita identificar o problema. Esta é a falha mais confusa desta funcionalidade. O erro 'dict object' has no attribute 'stdout' pertence a outro tipo de ignorância: uma tarefa ignorada porque o seu when era falso regista um resultado sem a chave stdout.
check_mode: false, e o único local onde pertence
check_mode: false numa tarefa significa "executar isto de facto, mesmo em --check". Esta é a correção para o problema dos comandos ignorados e só é segura numa tarefa que faça leituras.
- name: Read the installed app version
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
check_mode: false
changed_when: falseEssa tarefa é correta nos dois modos. Lê uma versão e nunca escreve, changed_when: false impede que reporte uma alteração que não fez e check_mode: false faz com que app_version.stdout contenha a versão real durante uma execução de teste, em vez de uma cadeia vazia. Assim, as condições baseadas nela continuam a ser avaliadas corretamente.
Leia a palavra-chave literalmente antes de a copiar para qualquer outro local. Uma tarefa com check_mode: false escreve nos seus servidores durante ansible-playbook --check. Coloque-a numa tarefa apt ou numa tarefa template para tornar uma execução de teste visualmente mais limpa, e essa execução já não será uma execução de teste. Quando não for possível tornar uma tarefa de escrita segura, aplique uma condição:
- name: Apply the database migration
ansible.builtin.command: /usr/local/bin/app migrate --apply
when: not ansible_check_modeansible_check_mode é uma variável especial que o Ansible define como true durante uma execução de verificação. Também existe a palavra-chave inversa. check_mode: true fixa uma tarefa no modo de verificação, mesmo durante uma execução real, transformando-a numa verificação de divergências: registe o resultado, e um relatório changed significa que o host já não corresponde ao que a tarefa exige.
Por que uma tarefa comunica alterações em todas as execuções
Execute o playbook duas vezes seguidas, sem nada entre as execuções. Todas as tarefas devem comunicar ok na segunda execução. Qualquer tarefa que continue a comunicar changed indica uma de duas situações: o módulo não consegue ver o estado que gere, ou a entrada fornecida não é estável. Ambas as situações podem ser corrigidas e nenhuma é ruído a silenciar.
commandeshellsemcreates,removesouchanged_whencomunicamchangedsempre, porque o módulo não tem como saber se alguma coisa aconteceu. Adicionecreatesou definachanged_whenpara uma string na saída.ansible.builtin.filecomstate: touchcomunicachangedem todas as execuções por definição, porque tocar num ficheiro atualiza os respetivos timestamps. Usestate: filese apenas pretendia definir o proprietário ou o modo.- Um
templatecujo resultado renderizado varia reescreve o ficheiro em todas as execuções. Um timestamp deansible_date_time, uma chamada anow()ou uma palavra-passe gerada de novo em cada execução produzem bytes diferentes, pelo que o módulo comunica corretamente uma alteração. Remova o valor variável do template. ansible.builtin.usercompassword: "{{ pw | password_hash('sha512') }}"muda em todas as execuções, porquepassword_hashescolhe um salt aleatório sempre que é chamado. Por isso, o hash resultante nunca corresponde ao que já existe em/etc/shadow. Passe um salt explícito derivado de algo estável.state: latestnum módulo de pacotes comunicachangedsempre que existe uma atualização disponível. Essa alteração é legítima. É também por isso questate: latestproduz um playbook cujo resultado não pode prever. Usestate: presente atualize de forma intencional.ansible.builtin.unarchiveapontado para um URL semcreatestransfere novamente o arquivo em todas as execuções. Para tipos de arquivo, também não consegue comparar com o destino, que volta a extrair. Forneça-lhe um caminhocreates.
--diff é a forma mais rápida de distinguir estes casos. Se uma tarefa indicar changed e o diff mostrar bytes diferentes, a sua entrada é instável. Se indicar changed e o diff não mostrar nada, o módulo não consegue expressar o que alterou. Normalmente, isso significa uma tarefa command ou uma escrita que altera apenas metadados, como um timestamp.
Não use changed_when: false para silenciar uma tarefa ruidosa. Essa opção suprime o relatório, pelo que notify nunca é acionado e o handler que reinicia o serviço nunca é executado. Corrija a tarefa.
Reduza o raio de impacto: --limit, --tags e --step
O modo de verificação mostra o que seria alterado. Estas opções determinam quantas máquinas tomam conhecimento das alterações de cada vez.
--limit restringe o play a um subconjunto do inventário. Aceita os mesmos padrões que hosts:, por isso tanto --limit web1 como --limit 'webservers:!web3' funcionam. Coloque o padrão entre aspas. Um ! sem aspas numa sessão interativa do bash ativa a expansão do histórico no ponto de exclamação, e a shell reescreve o comando antes de o Ansible o receber.
Confirme o padrão antes de confiar nele. ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts mostra os hosts correspondentes e termina sem estabelecer ligação a nenhum deles. Um padrão sem correspondências é seguro, porque o Ansible não recorre ao inventário completo. Mostra um aviso de que não conseguiu encontrar correspondências para o padrão de hosts e termina com um erro a indicar que o inventário, o padrão de hosts e --limit não deixam hosts para o destino. Saber como o ficheiro de inventário define esses grupos é o que torna um padrão previsível desde o início.
--tags deploy executa apenas as tarefas marcadas, e --skip-tags packages executa todas as restantes. --list-tags mostra o que está disponível. As tags tornam-se úteis quando um play ultrapassa o ponto em que já não quer executar tudo, o que também é uma das razões para dividir um playbook longo em roles.
--start-at-task "Write the site config" retoma uma execução falhada a partir de uma tarefa identificada pelo nome. Use-a para recuperar, mas tenha em conta o custo: tudo o que estiver antes dessa tarefa é ignorado, incluindo as tarefas que definem facts ou registam as variáveis lidas pelas tarefas seguintes.
--step pede confirmação antes de cada tarefa e espera que responda yes, no ou continue. É lento, mas é a ferramenta certa na primeira execução de algo destrutivo, porque permite parar entre duas tarefas, em vez de só depois de vinte.
Implantar a alteração de forma faseada
Por predefinição, o Ansible executa uma tarefa em cada host do play antes de iniciar a tarefa seguinte. Isto é rápido, mas significa que uma tarefa incorreta chega a toda a frota no mesmo segundo. Quando lê o erro e prime Ctrl-C, a alteração já está aplicada em todos os hosts.
serial divide o play em lotes. O play completo é executado no primeiro lote e, depois, no lote seguinte.
- name: Roll out the web tier
hosts: webservers
serial: [1, 5, "30%"]
max_fail_percentage: 0
tasks:
- name: Deploy the release
ansible.builtin.include_role:
name: webappO primeiro lote contém um host. Se ele não apresentar problemas, o segundo lote contém cinco hosts e cada lote seguinte contém 30 por cento dos hosts do play. max_fail_percentage: 0 interrompe o restante do play assim que qualquer host de um lote falha, porque o limite tem de ser excedido e qualquer falha excede zero. A garantia de começar com uma única máquina vem do primeiro lote, não dessa definição: uma release que falhe em todos os hosts falha no único host do lote um e para nesse ponto. Uma falha que só aparece no lote de cinco hosts já atingiu os restantes hosts desse lote, porque o Ansible executa cada tarefa em todo o lote antes de iniciar a tarefa seguinte. any_errors_fatal: true é a versão mais direta: termina o play para todos quando o primeiro host falha.
Executar primeiro num único host não é excesso de cautela. A razão é concreta. Os grupos do inventário sofrem alterações ao longo do tempo. Um servidor adicionado seis meses depois dos restantes pode executar uma release de distribuição diferente, ter um serviço instalado manualmente ou usar uma disposição de discos diferente. O playbook está correto para o grupo, mas incorreto para esse host, e um dry run num host já convergido não revela o problema. Gerir uma frota de servidores Linux consiste, em grande parte, em encontrar o host diferente antes que a alteração o afete.
A ordem para executar as ações
ansible-playbook site.yml --syntax-checkdeteta erros de YAML e de estrutura sem qualquer acesso à rede.ansible-playbook site.yml --limit web1 --list-hostsconfirma que o padrão corresponde ao que espera.ansible-playbook site.yml --limit web1 --check --diffé a execução de teste. Leia o diff.ansible-playbook site.yml --limit web1 --diffaplica a alteração a esse host.- Execute novamente o passo 4. Tudo deve indicar
ok. Qualquer item que continue comochangedé uma tarefa a corrigir antes de a alteração ser aplicada ao resto da frota. ansible-playbook site.yml --check --diffem todo o inventário passa a devolver uma resposta útil, porque os hosts convergidos ficam silenciosos e o que resta é a diferença real.
Há uma advertência sobre o passo 3. --diff imprime o conteúdo dos ficheiros no terminal e no log da tarefa de CI. Assim, se um template gerar uma palavra-passe de base de dados, essa palavra-passe será incluída no log. Defina diff: false nessa tarefa para suprimir a saída ou no_log: true para ocultar todo o resultado. Mantenha o próprio valor num ficheiro Ansible Vault encriptado, e não no repositório.
FAQ
O ansible-playbook --check altera alguma coisa no servidor?
Não, com uma exceção que pode controlar. No modo de verificação, é pedido a cada módulo que comunique o que faria em vez de escrever, e os módulos que não conseguem fazer isso não comunicam nada nem executam alterações. A exceção é a palavra-chave de tarefa check_mode: false, que força essa tarefa específica a ser executada de facto, mesmo durante uma execução --check. Procure check_mode: false nos seus playbooks e roles antes de confiar numa execução de simulação e confirme que cada ocorrência corresponde a uma tarefa que apenas lê o estado.
Qual é a diferença entre --check e --diff?
--check determina se alguma coisa é executada de facto. --diff determina a quantidade de detalhe apresentada. --check, por si só, informa que um ficheiro seria alterado. --diff, por si só, aplica a alteração e mostra as linhas que alterou. Use as duas opções em conjunto para obter uma simulação que possa realmente ler e mantenha --diff ativo também nas execuções reais, definindo always = true em [diff] dentro de ansible.cfg.
Porque é que a minha tarefa Ansible indica changed em todas as execuções?
Porque o módulo não consegue ver o estado que gere ou porque o valor fornecido é diferente em cada execução. command e shell comunicam changed sempre, exceto se adicionar creates ou changed_when. file com state: touch altera o estado por definição. Um template que renderiza um timestamp ou uma palavra-passe gerada de novo produz bytes diferentes em cada execução, pelo que o ficheiro é realmente reescrito. Execute o playbook duas vezes seguidas: tudo o que continuar como changed na segunda passagem corresponde à tarefa que deve corrigir.
Porque é que as minhas tarefas command e shell são ignoradas durante uma simulação?
Porque não existe uma forma só de leitura para executar um comando arbitrário. No modo de verificação, o módulo command define skipped: true com a mensagem Command would have run if not in check mode. Adicione creates ou removes para que o modo de verificação possa avaliar o teste ao ficheiro. Numa tarefa que apenas lê o estado, defina check_mode: false juntamente com changed_when: false, para que o resultado registado contenha a saída real durante a simulação e as condições baseadas nesse resultado continuem a funcionar.
Porque é que o modo de verificação falha num servidor novo, mas funciona num servidor existente?
Porque o modo de verificação não cria o estado de que as tarefas seguintes dependem. Numa simulação dirigida a um host sem nginx, a instalação é indicada como changed e a execução falha depois na tarefa que escreve em /etc/nginx/conf.d/, porque esse diretório nunca foi criado. Este comportamento é esperado. O modo de verificação deteta desvios em hosts nos quais o playbook já convergiu. Não consegue validar uma primeira execução. Num host novo, aplique o playbook a uma máquina e leia os resultados da segunda execução.