SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-21

Ansible: templates e handlers com exemplo prático

Gere a configuração do nginx com Jinja2, recarregue apenas após mudanças reais e execute o playbook duas vezes para comprovar a idempotência.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 20, 2026.

O que os templates e os handlers do Ansible acrescentam ao seu primeiro playbook

Os templates e os handlers do Ansible são os dois componentes que transformam um playbook estático num playbook útil. Um template gera um ficheiro de configuração a partir das suas variáveis, para que um único ficheiro cubra todos os hosts. Um handler é executado apenas quando uma task altera efetivamente alguma coisa. Assim, o serviço é recarregado quando a configuração muda e permanece inalterado no resto do tempo.

Este guia continua exatamente onde o seu primeiro playbook do Ansible num VPS termina. Já tem um play que instala um pacote e inicia um serviço. Tudo o que se segue é executado numa máquina, porque o play tem como destino localhost através de uma ligação local. Não precisa de um segundo servidor para acompanhar o guia. O mesmo play é executado contra hosts reais do inventário sem alterar as tasks. A última secção explica o que muda.

Configurar o diretório de trabalho

sudo apt update
sudo apt install -y ansible nginx
ansible --version
mkdir -p ~/ansible-templates/templates
cd ~/ansible-templates

nginx está presente apenas porque é um serviço real, com um ficheiro de configuração e um comando de reload. Isso é tudo o que o exemplo precisa. ansible --version mostra a versão do ansible-core e o interpretador Python que será utilizado. Anote ambos. O playbook abaixo usa nomes de módulos totalmente qualificados, como ansible.builtin.template, que exigem o Ansible 2.10 ou posterior. Qualquer pacote atual da distribuição já é muito mais recente.

Crie inventory.ini:

[local]
localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"

ansible_connection=local indica ao Ansible para executar cada tarefa como um processo local, em vez de abrir uma sessão SSH para o próprio sistema. A segunda definição não é decorativa. Quando escreve localhost num ficheiro de inventário, este passa a ser um host comum. Nesse caso, perde o interpretador que o Ansible fornece automaticamente ao localhost implícito. O Ansible recorre então à descoberta do interpretador e pode escolher um Python diferente daquele que está a executar o play. ansible_playbook_python é o interpretador que está a executar ansible-playbook neste momento. Assim, ambos permanecem alinhados.

Crie ansible.cfg:

[defaults]
inventory = inventory.ini

Sem esse ficheiro, terá de indicar -i inventory.ini em todos os comandos. Sem qualquer inventário, o Ansible mostra [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all', e um play com hosts: all não encontra correspondências. Há mais um detalhe sobre ansible.cfg: o Ansible ignora-o quando está num diretório com permissões de escrita para todos os utilizadores. Por isso, mantenha o projeto no seu diretório pessoal. Um ficheiro de inventário contém mais do que uma lista de hosts, e este é o ficheiro mínimo necessário.

template versus copy e quando usar cada um

ansible.builtin.copy transfere um ficheiro tal como está. ansible.builtin.template processa primeiro o ficheiro com Jinja2 e transfere o resultado. A documentação do módulo descreve template como “um módulo virtual totalmente implementado como um plugin de ação e executado no controlador”. Isto tem uma consequência importante: a renderização ocorre na máquina onde introduziu ansible-playbook. O host de destino nunca vê as suas variáveis e não precisa de ter Jinja2 instalado.

Use copy quando o ficheiro for idêntico em todos os hosts. Use template assim que um valor variar por host ou quando precisar de um ciclo {% for %} ou de um bloco {% if %}. copy tem um parâmetro content:, e as variáveis nele contidas são substituídas como em qualquer outro argumento de tarefa, mas não existem ciclos nem condicionais nesse parâmetro. Por isso, qualquer conteúdo com estrutura deve ficar num template. Ambos os módulos aceitam as mesmas opções de ficheiro, porque ambos incorporam os mesmos fragmentos da documentação. Assim, owner, group, mode, backup e validate comportam-se da mesma forma em ambos.

Escreva o template: uma variável, um loop

Guarde isto como templates/app.conf.j2:

# {{ ansible_managed }}
upstream {{ app_name }}_backend {
{% for backend in app_backends %}
    server {{ backend.host }}:{{ backend.port }} weight={{ backend.weight }};
{% endfor %}
}

server {
    listen {{ app_listen_port }};
    server_name {{ app_server_name }};

    location / {
        proxy_pass http://{{ app_name }}_backend;
        proxy_set_header Host $host;
    }
}

Dois tipos de tags Jinja2 fazem o trabalho aqui. {{ ... }} é uma expressão e imprime o seu valor. {% ... %} é uma instrução e não imprime nada por si só. app_backends é uma lista de dicionários, por isso backend.host lê uma chave de cada entrada e o loop escreve uma linha server por entrada, independentemente do número que definir.

Há um detalhe sobre os espaços em branco, porque surpreende quem conhece Jinja2 de outros contextos. O Ansible define trim_blocks como yes por predefinição, algo que o próprio Jinja2 não faz. Por isso, a quebra de linha imediatamente depois de uma tag {% ... %} é removida e o loop não deixa uma linha em branco atrás de si. O Ansible mantém lstrip_blocks com o valor no, portanto todos os espaços que colocar antes de uma tag {% são mantidos e aparecem no ficheiro renderizado. Se o resultado tiver indentação inesperada, defina lstrip_blocks: true na tarefa do template.

Por predefinição, {{ ansible_managed }} é renderizado como o texto literal Ansible managed. Mantenha esse comportamento. É comum redefinir ansible_managed em ansible.cfg para incluir uma data. Quando isso acontece, o ficheiro renderizado é diferente em todas as execuções, a tarefa comunica uma alteração em todas as execuções e o serviço é recarregado em todas as execuções. Essa única definição elimina a propriedade em que o resto deste guia se baseia. A extensão .j2 é uma convenção, e o Ansible não a verifica.

O playbook

Guarde isto como site.yml:

- name: Render an nginx site from a template
  hosts: local
  become: true

  vars:
    app_name: learn
    app_listen_port: 8080
    app_server_name: learn.example.com
    app_backends:
      - host: 127.0.0.1
        port: 9001
        weight: 3
      - host: 127.0.0.1
        port: 9002
        weight: 1

  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Render the site configuration
      ansible.builtin.template:
        src: templates/app.conf.j2
        dest: "/etc/nginx/conf.d/{{ app_name }}.conf"
        owner: root
        group: root
        mode: '0644'
        backup: true
      notify: nginx config changed

    - name: Make sure nginx is enabled and running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Test the nginx configuration
      ansible.builtin.command:
        cmd: /usr/sbin/nginx -t
      changed_when: false
      listen: nginx config changed

    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
      listen: nginx config changed

mode: '0644' está entre aspas de forma deliberada. A documentação das opções de ficheiros recomenda colocar os números octais entre aspas «para que o Ansible receba uma string e possa fazer a sua própria conversão de string para número». Sem aspas, o parser YAML interpreta 0644 como um número simples, e pode acabar com permissões diferentes das pretendidas.

notify: nginx config changed identifica um tópico, não um handler. Ambos os handlers contêm listen: nginx config changed, por isso uma notificação aciona os dois. Se adicionar mais tarde um terceiro handler com a mesma linha listen, não será necessário alterar a tarefa do template. cache_valid_time: 3600 impede que uma segunda execução durante a mesma hora volte a contactar os mirrors de pacotes.

Execute uma vez e leia o que foi apresentado

ansible-playbook site.yml

Se o sudo pedir uma palavra-passe, adicione -K e o Ansible irá pedi-la.

Leia primeiro as linhas de cada tarefa e, em seguida, o PLAY RECAP no final. Cada tarefa apresenta changed: quando o Ansible teve de fazer alguma alteração, ou ok: quando o host já estava no estado pretendido. O resumo totaliza esses contadores por host. Só depois de todas as tarefas do play terem terminado, e não antes, verá RUNNING HANDLER [Test the nginx configuration] seguido de RUNNING HANDLER [Reload nginx].

Agora verifique a própria máquina em vez de confiar no resultado apresentado:

sudo cat /etc/nginx/conf.d/learn.conf
sudo /usr/sbin/nginx -t
curl -sI http://127.0.0.1:8080/

nginx -t apresenta nginx: configuration file /etc/nginx/nginx.conf test is successful quando a configuração montada é válida. curl devolve uma linha de estado do nginx, e 502 Bad Gateway é a resposta correta neste caso, porque o server block está ativo e não há nada a escutar nas portas 9001 ou 9002. sudo tail /var/log/nginx/error.log indica o motivo em linguagem simples: connect() failed (111: Connection refused) while connecting to upstream.

Execute novamente para comprovar a idempotência

ansible-playbook site.yml

Esta é a execução relevante. Compare o resultado com o da primeira execução, linha a linha. A tarefa de template deve agora apresentar ok: onde apresentou changed:, e nenhum dos handlers deve aparecer no resultado.

O mecanismo é simples e é importante conhecê-lo, porque é com base nele que os problemas são depurados. template renderiza o ficheiro no controller e compara o checksum do resultado com o checksum do ficheiro que já está em dest. Conteúdo, proprietário e modo iguais significam que não há nada a fazer. Por isso, a tarefa apresenta ok, notify nunca é acionado e o handler nunca é executado. Os handlers são acionados em changed e em nenhuma outra situação.

Comprove também o sentido inverso. Altere weight: 3 para weight: 1 em vars, execute novamente o play e a tarefa de template apresentará changed. Ambos os handlers serão executados, e sudo cat /etc/nginx/conf.d/learn.conf apresentará o novo valor.

Se uma segunda execução idêntica continuar a indicar uma alteração, o resultado renderizado não é estável. Comece por procurar no resultado algo baseado no tempo, porque essa é a causa mais comum e um ansible_managed personalizado é normalmente o responsável. Depois, confirme se mode e owner na tarefa correspondem ao que está efetivamente no disco, porque uma diferença nesses valores constitui uma alteração mesmo quando os bytes são idênticos.

Veja a alteração antes de a aplicar

ansible-playbook site.yml --check --diff

--check executa o play sem alterar o host. --diff mostra o que cada tarefa teria alterado. No caso de template, apresenta uma diferença linha a linha entre o conteúdo renderizado e o ficheiro no disco. Em conjunto, estes modos respondem à pergunta "o que faria esta execução" sem executar as alterações. O modo de verificação também tem limitações, sobretudo nas tarefas cujo resultado depende de uma tarefa anterior que o modo de verificação não executou efetivamente.

Por que os handlers aguardam até ao fim do play

A documentação dos handlers é clara: "Por predefinição, os handlers são executados depois de todas as tasks de um determinado play terem sido concluídas. Os handlers notificados são executados automaticamente depois de cada uma das seguintes secções, pela ordem indicada: pre_tasks, roles/tasks e post_tasks."

A razão é o agrupamento. Um play que gera quatro ficheiros de configuração para um serviço deve reiniciar esse serviço uma vez, no fim, quando os quatro ficheiros já estiverem disponíveis. Reiniciá-lo depois de cada ficheiro faria com que fosse reiniciado quatro vezes, e três desses reinícios carregariam uma configuração incompleta. A mesma página indica claramente esta garantia: "Notificar o mesmo handler várias vezes faz com que o handler seja executado apenas uma vez, independentemente do número de tasks que o notificam."

A ordem também é fixa: "Os handlers são executados pela ordem em que são definidos na secção handlers, e não pela ordem indicada na instrução notify." É por isso que Test the nginx configuration aparece antes de Reload nginx no playbook. O teste é executado primeiro porque está escrito primeiro, e nada na linha notify altera esse comportamento.

Como executar handlers antecipadamente e como executá-los depois de uma falha

Por vezes, uma tarefa posterior no mesmo play precisa que o serviço já esteja a executar a nova configuração. Nesse ponto, execute os handlers notificados com o módulo meta, que a documentação descreve como fazendo com que "o Ansible execute todas as tarefas de handler que tenham sido notificadas até ao momento".

    - name: Run the notified handlers now instead of at the end of the play
      ansible.builtin.meta: flush_handlers

    - name: Wait for the new listener to accept connections
      ansible.builtin.wait_for:
        host: 127.0.0.1
        port: 8080
        timeout: 10

Remova essa linha meta e a tarefa wait_for será executada enquanto o nginx ainda estiver a servir a configuração antiga. Numa primeira execução, ainda não existe nenhum listener na porta 8080, pelo que a tarefa espera os dez segundos completos e depois falha.

O segundo caso é uma falha. "Se uma tarefa notificar um handler, mas outra tarefa falhar mais tarde no play, por predefinição o handler não é executado nesse host, o que pode deixar o host num estado inesperado." Um play que gere uma configuração e depois falhe numa tarefa não relacionada deixa, por isso, o novo ficheiro no disco, mas mantém a configuração antiga carregada no serviço em execução. Substitua esse comportamento por --force-handlers na linha de comandos ou por force_handlers: true no play. A mesma opção existe como force_handlers = True em [defaults] em ansible.cfg e como a variável de ambiente ANSIBLE_FORCE_HANDLERS. O valor predefinido é False.

Os nomes dos handlers entram em conflito, e o que perde falha silenciosamente

A documentação estabelece a regra: "Cada handler deve ter um nome globalmente único. Se forem definidos vários handlers com o mesmo nome, apenas o último carregado no play pode ser notificado e executado." Os handlers definidos dentro de uma role também não ficam limitados a essa role. São inseridos numa lista global de handlers para todo o play. Assim, duas roles que definam Restart nginx deixam um nome que resolve exatamente para um deles. A ordem de carregamento decide qual, e não a role a partir da qual fez a notificação.

Teste essa regra antes de depender dela. Guarde isto como handlers-dup.yml:

- name: Two handlers, one name
  hosts: local
  gather_facts: false

  tasks:
    - name: Notify the duplicated name
      ansible.builtin.command:
        cmd: /bin/true
      changed_when: true
      notify: Duplicated handler

  handlers:
    - name: Duplicated handler
      ansible.builtin.file:
        path: /tmp/dup-first
        state: touch
        mode: '0644'

    - name: Duplicated handler
      ansible.builtin.file:
        path: /tmp/dup-second
        state: touch
        mode: '0644'
rm -f /tmp/dup-first /tmp/dup-second
ansible-playbook handlers-dup.yml
ls -l /tmp/dup-first /tmp/dup-second

O play é concluído com sucesso, RUNNING HANDLER [Duplicated handler] aparece uma vez, e ls imprime uma linha para /tmp/dup-first e ls: cannot access '/tmp/dup-second': No such file or directory para o outro. O handler executado é o que foi escrito primeiro, não o último carregado. Isto é o oposto do que essa frase prevê.

É importante compreender a diferença, porque a regra documentada refere-se a blocos de handlers, e não às linhas de um ficheiro. Os handlers provenientes de locais separados, primeiro de uma role e depois de outra, pertencem a blocos separados, e um bloco posterior oculta um bloco anterior. Uma lista simples de handlers: num play é um único bloco. A pesquisa dentro de um bloco decorre de cima para baixo e para no primeiro nome correspondente. Portanto, dentro de um ficheiro, a primeira definição é usada e a segunda fica inacessível. Entre roles, a ocultação funciona como descrito na documentação. Em qualquer dos casos, nunca consegue alcançar ambos, e nenhuma das duas formas deve servir de base para uma configuração.

Existem duas formas simples de resolver o problema. Atribua a cada handler um nome com um prefixo específico da role, ou faça a notificação usando a forma qualificada role_name : handler_name. A documentação apresenta esta forma como uma maneira de "garantir que um handler de uma role seja notificado, em vez de um handler externo à role com o mesmo nome". Os espaços em redor dos dois-pontos fazem parte da sintaxe. Isto torna-se um problema real assim que começar a incluir roles que não escreveu.

Existe mais uma regra na mesma página: "Evite colocar variáveis no nome do handler. Como os nomes dos handlers são convertidos em templates antecipadamente, o Ansible pode não ter um valor disponível para um nome de handler como este." Um handler chamado Restart {{ service_name }} faz falhar todo o play quando essa variável não está definida no momento em que o nome é convertido em template. Manter os nomes dos handlers como strings fixas e agrupá-los com listen evita essa questão.

validate: recusar a instalação de um ficheiro renderizado inválido

validate executa um comando no ficheiro renderizado antes de o Ansible o mover para o destino final. A documentação diz: "O comando de validação a executar antes de copiar o ficheiro atualizado para o destino final. É usado um caminho de ficheiro temporário para a validação, passado através de %s, que tem de estar presente, como nos exemplos abaixo. O comando também é passado de forma segura, pelo que funcionalidades da shell, como expansão e pipes, não funcionam."

Dessa descrição resultam duas regras. %s é obrigatório, e uma string de validação sem esse marcador faz a tarefa falhar com validate must contain %s. Além disso, não existe uma shell, pelo que pipes, redirecionamento, expansão de padrões e && não funcionam. Um comando, um argumento de ficheiro.

Os exemplos oficiais do módulo mostram os dois casos em que isto funciona corretamente:

- name: Copy a new sudoers file into place, after passing validation with visudo
  ansible.builtin.template:
    src: /mine/sudoers
    dest: /etc/sudoers
    validate: /usr/sbin/visudo -cf %s

- name: Update sshd configuration safely, avoid locking yourself out
  ansible.builtin.template:
    src: etc/ssh/sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: '0600'
    validate: /usr/sbin/sshd -t -f %s
    backup: yes

Ambos funcionam porque cada verificador recebe um ficheiro e avalia-o de forma independente. visudo -cf lê um ficheiro sudoers. sshd -t -f lê um sshd_config completo.

Por que validate não consegue verificar o ficheiro do nginx neste guia

Adicione validate: /usr/sbin/nginx -t -c %s à tarefa template acima e a tarefa falha. A mensagem indica a causa:

nginx: [emerg] "upstream" directive is not allowed here in <ansible temporary path>:2

nginx -t -c espera uma configuração completa que comece no nível superior com os blocos events e http. O ficheiro que este play gera é um fragmento, incluído no bloco http por include /etc/nginx/conf.d/*.conf; dentro de /etc/nginx/nginx.conf. Isolado desse contexto, upstream é realmente uma diretiva no local errado, pelo que o nginx rejeita um ficheiro que está correto no local onde é efetivamente usado. O verificador recebeu um fragmento e foi instruído a tratá-lo como uma configuração completa.

A solução funcional é a que já existe no playbook. Instale o fragmento e, em seguida, verifique a configuração montada num handler definido acima do handler de reload. Como os handlers são executados pela ordem em que são definidos, nginx -t analisa o /etc/nginx/nginx.conf real com o seu fragmento incluído. Se essa verificação falhar, o play falha antes de systemctl reload ser chamado. Tenha em conta o custo: o ficheiro incorreto fica no disco quando essa verificação falha, e o nginx continua a servir a última configuração que carregou até alguém o reiniciar.

É para isso que backup: true é necessário. Antes de substituir o ficheiro original, ele grava uma cópia do ficheiro anterior no mesmo diretório, com o nome basename.PID.YYYY-MM-DD@HH:MM:SS~. Assim, o diretório fica com entradas como learn.conf.4127.2026-08-20@11:42:09~. Execute sudo ls -l /etc/nginx/conf.d/ depois de uma alteração e encontrará uma.

Esse detalhe do nome é mais importante do que parece. A cópia de segurança é inofensiva em /etc/nginx/conf.d/ porque a configuração principal inclui apenas conf.d/*.conf e o nome da cópia termina com um til. Isso não é inofensivo num diretório incluído com um * simples. No Debian e no Ubuntu, /etc/nginx/nginx.conf inclui /etc/nginx/sites-enabled/* exatamente dessa forma. Se usar backup: true para gerar um template em sites-enabled, o nginx carrega a cópia como um segundo server block ativo. É por isso que este play escreve em conf.d.

Executar o mesmo play nos hosts reais do inventário

Altere hosts: local para o nome do grupo que utiliza, e não altere mais nada no play. O template é renderizado uma vez por host, por isso app_listen_port e app_backends podem vir de group_vars e host_vars, enquanto o próprio ficheiro do template permanece único. Essa é a vantagem de colocar os valores em variáveis, e não no ficheiro.

Há duas alterações. become: true passa a precisar de uma palavra-passe sudo em cada destino, exceto se tiver sudo sem palavra-passe nesse destino; nesse caso, adicione -K. Além disso, qualquer segredo nesse template, como uma palavra-passe de base de dados ou um token de API, não pode ficar em texto simples em vars: num ficheiro que seja submetido ao controlo de versões. Encripte esses valores com o Ansible Vault e faça referência a eles pelo nome, exatamente como faz agora, porque o template não depende da origem da variável.

Quando o play crescer para além de um serviço, vars:, templates/ e handlers: já têm um local padrão próprio. Movê-los para esses locais é o objetivo de separar um playbook de uma role.

FAQ

Por que o meu handler do Ansible não foi executado?

Quase sempre porque a tarefa que o notificaria reportou ok em vez de changed. Os handlers são executados quando há alterações e em nenhuma outra situação. Por isso, uma tarefa template cujo resultado é igual ao ficheiro que já está no disco não notifica nada. Depois, verifique quatro pontos. A string em notify deve corresponder exatamente a name do handler ou a um tópico listen, incluindo maiúsculas, minúsculas e espaços. Uma tarefa posterior que falhe nesse host suprime os handlers notificados, a menos que passe --force-handlers. Um handler definido noutro play não está visível neste play. E uma tarefa notificadora ignorada por uma condição when nunca notifica.

Por que o meu playbook reporta alterações em todas as execuções?

O texto renderizado não é igual entre execuções. A causa mais comum é um timestamp na saída. Uma string ansible_managed personalizada que inclua uma data produz exatamente esse efeito. A seguir, verifique mode e owner na tarefa. Se não corresponderem ao ficheiro que já está no disco, o Ansible corrige-os e reporta uma alteração, mesmo que o conteúdo seja idêntico. Execute ansible-playbook site.yml --check --diff para saber qual dos dois casos ocorre, porque --diff mostra a diferença que a tarefa pretende aplicar.

Qual é a diferença entre template e copy no Ansible?

ansible.builtin.copy envia um ficheiro sem alterações. ansible.builtin.template renderiza-o com Jinja2 no controller e só depois envia o resultado. Assim, as variáveis e os ciclos são resolvidos antes de o ficheiro chegar ao host de destino. Use copy para um ficheiro que tenha de ser idêntico byte a byte em todos os hosts. Use template para qualquer ficheiro que varie por host. Ambos partilham as mesmas opções de ficheiro. Por isso, mode, owner, backup e validate funcionam da mesma forma nos dois.

Como faço um handler executar a meio de um play?

Adicione ansible.builtin.meta: flush_handlers como uma tarefa no ponto em que pretende executar os handlers. Isto executa todos os handlers notificados até esse momento. Depois, o play continua normalmente. Use-o quando uma tarefa posterior no mesmo play depender de o serviço já estar a executar a nova configuração. Por exemplo, uma wait_for numa porta que só exista depois do reload. Esta é a forma suportada de executar um handler antes do fim do play.

Posso usar validate com um fragmento de configuração do nginx?

Não com nginx -t -c %s. Esse comando espera uma configuração completa que comece pelos blocos de nível superior events e http. Por isso, rejeita um fragmento conf.d com uma mensagem semelhante a "upstream" directive is not allowed here. O fragmento é válido dentro do bloco http, mas é inválido isoladamente. Instale o ficheiro e execute depois nginx -t contra a configuração montada, num handler definido antes do handler de reload. Os handlers são executados pela ordem em que são definidos. Assim, uma configuração inválida interrompe o play antes de a recarga ser tentada. Defina backup: true na tarefa template para que o ficheiro anterior continue disponível para reposição.

#ansible#jinja2#handlers#idempotence#automation