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.
Execute a simulação: --check e --diff
ansible-playbook -i inventory.ini site.yml --check --diff --limit web1-C e -D são as formas abreviadas das duas opções. O --limit é intencional. O diff de um host é algo que consegue ler. O diff de vinte hosts é algo que percorre sem ler.
Quatro palavras de resultado abrangem todo o relatório.
ok: [web1]significa que o módulo verificou o estado e que este já corresponde ao pretendido. Nada seria alterado.changed: [web1]significa que o módulo teria escrito alguma coisa. Com--diff, as linhas anteriores mostram o quê.skipping: [web1]significa que a tarefa não foi avaliada. Umwhenera falso ou o módulo não consegue ser executado em 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á com problemas.
--diff mostra um diff unificado para módulos de ficheiros. As linhas removidas são marcadas com - e as adicionadas com +. O cabeçalho começa com linhas que usam --- before e +++ after e identifica o caminho de destino. Os módulos que não escrevem ficheiros mostram o seu próprio estado anterior e posterior. Nesse caso, ansible.builtin.user mostra os atributos que seriam alterados, e não o conteúdo do ficheiro.
Ative o diff permanentemente em ansible.cfg para não se esquecer da opção:
[diff]
always = true
context = 5Há duas verificações mais rápidas 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 qualquer host. ansible-playbook site.yml --list-tasks mostra as tarefas que seriam executadas. Assim, pode descobrir que uma role que julgava ter uma tag não tem. Nenhuma das duas opções estabelece uma ligação, por isso ambas são imediatas.
O próprio modo de verificação estabelece ligações. Abre uma ligação SSH para cada host incluído no padrão e recolhe factos. Por isso, um host desligado faz a simulação falhar. Este resultado é útil por si só. Também explica por que decidir o que um playbook deve fazer com hosts inacessíveis é importante antes de incluir uma simulação 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 tem nginx, e a maior parte das tarefas vai 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 normal 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 que possa ser consultada. Nenhuma destas falhas é um erro no playbook. A execução de teste ficou sem 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.
A versão honesta da regra é esta: o modo de verificação é preciso num host que o playbook já convergiu e produz ruído num host novo. Uma execução com --check em que todas as tarefas informam ok é uma afirmação real sobre um host convergido, porque significa que nada seria alterado. Num host totalmente 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 para 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. A saída mostra skipping: [web1].
A documentação do módulo classifica o suporte ao modo de verificação como "parcial". A solução indicada é creates e removes. Forneça à tarefa um caminho creates. 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 válida. Se o caminho não existir, obtém Command would have run if not in check mode. Essa também é uma resposta válida. Sem creates, essa tarefa fica sem resultado no teste a seco.
O efeito secundário é pior do que a ausência de resultado. Uma tarefa ignorada continua a registar um resultado. No entanto, esse resultado indica uma tarefa ignorada e não tem a chave stdout. A condição da tarefa seguinte falha durante a avaliação, com um erro semelhante a 'dict object' has no attribute 'stdout'. O playbook funciona numa execução real, mas falha no teste a seco. Esta é a falha mais confusa de toda esta funcionalidade.
check_mode: false, e o único local onde ele pertence
check_mode: false numa tarefa significa "executar de verdade, mesmo em --check". Esta é a correção para o problema dos comandos ignorados e só é segura numa tarefa que faz 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 ela reporte uma alteração que não fez e check_mode: false faz com que app_version.stdout exista durante uma execução de teste, para que as condições baseadas nele continuem a ser avaliadas.
Leia a palavra-chave literalmente antes de a colar noutro 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 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, proteja-a com 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 reporta alterações em todas as execuções
Execute o playbook duas vezes seguidas, sem nada entre as execuções. Todas as tarefas devem reportar ok na segunda execução. Qualquer tarefa que continue a reportar changed está a indicar 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 são corrigíveis e nenhuma é ruído a silenciar.
commandeshellsemcreates,removesouchanged_whenreportamchangedtodas as vezes, porque o módulo não tem como saber se algo aconteceu. Adicionecreatesou definachanged_whenpara uma string presente na saída.ansible.builtin.filecomstate: touchreportachangedem todas as execuções por definição, porque tocar num ficheiro atualiza os respetivos timestamps. Usestate: filese o objetivo era apenas definir o proprietário ou o modo.- Um
templatecuja saída renderizada muda 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 produz bytes diferentes, por isso o módulo reporta corretamente uma alteração. Retire 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 e o hash resultante nunca corresponde ao que já está em/etc/shadow. Passe um salt explícito, derivado de algo estável.state: latestnum módulo de pacotes reportachangedsempre que existe uma atualização disponível. Nesse caso, o resultado está correto. É também por isso questate: latestproduz um playbook cujo resultado não pode prever. Usestate: presente faça as atualizações de forma deliberada.ansible.builtin.unarchiveapontado para um URL semcreatesvolta a descarregar e a extrair o conteúdo. Forneça um caminhocreates.
--diff é a forma mais rápida de distinguir estes casos. Se uma tarefa indicar changed e o diff mostrar bytes diferentes, a entrada não é estável. Se indicar changed e o diff não mostrar nada, o módulo não consegue expressar o que alterou, o que normalmente significa uma tarefa command ou uma escrita apenas de metadados, como a atualização de um timestamp.
Não use changed_when: false para silenciar uma tarefa ruidosa. Isto suprime o relatório, por isso notify nunca é acionado e o handler que reinicia o serviço nunca é executado. Corrija a tarefa.
Reduza o impacto: --limit, --tags e --step
O modo de verificação mostra o que seria alterado. Estas flags determinam quantas máquinas ficam sujeitas às alterações de cada vez.
--limit limita 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 apresenta 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 inteiro. Apresenta um aviso de que não conseguiu encontrar correspondências para o padrão de hosts e termina com um erro a indicar que os hosts e --limit não correspondem a nenhum host. 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 apresenta o que está disponível. As tags são úteis quando um play ultrapassa o ponto em que é aceitável executá-lo todo. Esta é 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-o para recuperar a execução, mas compreenda o custo: tudo o que estiver antes dessa tarefa é ignorado, incluindo tarefas que definem facts ou registam as variáveis lidas pelas tarefas seguintes.
--step pede confirmação antes de cada tarefa e aguarda uma resposta yes, no ou continue. É lento e é a ferramenta certa na primeira execução de uma operação destrutiva, porque permite parar entre duas tarefas em vez de parar depois de vinte.
Implante a alteração em série
Por padrão, o Ansible executa uma tarefa em todos os hosts do play antes de iniciar a tarefa seguinte. Isso é rápido, mas significa que uma tarefa incorreta chega a toda a frota no mesmo segundo. Quando lê o erro e pressiona Ctrl-C, a alteração já foi aplicada em todos os hosts.
serial divide o play em lotes. O play inteiro é executado no primeiro lote e depois no 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 tem um host. Se ele funcionar corretamente, o segundo lote terá cinco hosts, e cada lote seguinte terá 30 por cento dos hosts do play. max_fail_percentage: 0 termina o play assim que qualquer host de um lote falha, impedindo que uma versão com problemas avance além de uma máquina. any_errors_fatal: true é a versão mais abrangente: termina o play para todos na primeira falha de um host.
Executar primeiro em um host não é paranoia. O motivo é específico. Os grupos do inventário sofrem alterações ao longo do tempo. Um servidor adicionado seis meses depois dos outros pode usar uma versão diferente da distribuição, ter um serviço instalado manualmente por alguém ou usar uma disposição diferente dos discos. O playbook está correto para o grupo, mas incorreto para esse host, e um teste preliminar num host já convergido não revelará o problema. Gerir uma frota de servidores Linux consiste, em grande parte, em encontrar o host diferente antes que a alteração o atinja.
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 que o ansible-playbook --check altera no servidor?
Não altera nada, com uma exceção que pode controlar. No modo de verificação, cada módulo é instruído a gerar um relatório em vez de escrever, e os módulos que não conseguem fazer isso não reportam nem executam alterações. A exceção é a palavra-chave de tarefa check_mode: false, que força a execução real dessa tarefa mesmo durante uma execução --check. Procure check_mode: false nos seus playbooks e roles antes de confiar numa execução de teste 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 alteração é executada de facto. --diff determina a quantidade de detalhes apresentada. --check, por si só, informa que um ficheiro seria alterado. --diff, por si só, aplica a alteração e mostra as linhas alteradas. Use ambas para obter uma execução de teste que possa ser lida e mantenha --diff ativo também nas execuções reais, definindo always = true em [diff] dentro de ansible.cfg.
Porque é que a minha tarefa do 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 reportam sempre changed, exceto se adicionar creates ou changed_when. file com state: touch altera o estado por definição. Um template que gere um timestamp ou uma palavra-passe recém-gerada produz bytes diferentes em cada execução, pelo que o ficheiro é realmente reescrito. Execute o playbook duas vezes seguidas: tudo o que continuar changed na segunda passagem corresponde à tarefa que deve corrigir.
Porque é que as minhas tarefas command e shell são ignoradas durante uma execução de teste?
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 do ficheiro. Numa tarefa que apenas lê o estado, defina check_mode: false juntamente com changed_when: false, para que o resultado registado continue a existir durante a execução de teste e as condições baseadas nele 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 execução de teste contra um host sem nginx, a instalação é reportada como changed e a tarefa que escreve em /etc/nginx/conf.d/ falha, porque esse diretório nunca foi criado. Este comportamento é esperado. O modo de verificação deteta divergências em hosts que o playbook já convergiu. Não consegue validar a primeira execução. Num host novo, aplique o playbook a uma máquina e analise a segunda execução.