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

Templates e handlers no Ansible 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 confirmar 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 as duas peças que transformam um playbook estático num playbook útil. Um template gera um ficheiro de configuração a partir das suas variáveis, pelo que um único ficheiro cobre 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 a partir de o seu primeiro playbook Ansible num VPS. 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 visa 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 de um inventário real sem alterações nas tasks. A última secção explica o que muda.

Configure 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 aparece aqui 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 requerem Ansible 2.10 ou posterior. Qualquer pacote atual da distribuição já está muito além dessa versão.

Crie inventory.ini:

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

ansible_connection=local instrui o Ansible a executar cada tarefa como um processo local, em vez de abrir uma sessão SSH para o próprio sistema. A segunda configuração não é meramente decorativa. Quando escreve localhost num ficheiro de inventário, ele 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 executa o play. ansible_playbook_python é o interpretador que executa ansible-playbook neste momento. Assim, ambos permanecem alinhados.

Crie ansible.cfg:

[defaults]
inventory = inventory.ini

Sem esse ficheiro, terá de passar -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 corresponde a nenhum host. Há mais um detalhe sobre ansible.cfg: o Ansible ignora-o quando está num diretório com permissões de escrita para todos. 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 menor ficheiro que cumpre o objetivo.

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 nunca precisa de ter o 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 qualquer outro argumento de tarefa, mas não existem ciclos nem condicionais nesse conteúdo. Por isso, qualquer configuração com estrutura deve ficar num template. Ambos os módulos aceitam as mesmas opções de ficheiro, porque ambos incluem os mesmos fragmentos de 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 este conteúdo 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 de entradas definido.

Há um detalhe sobre os espaços em branco que surpreende quem conhece Jinja2 de outros contextos. O Ansible define trim_blocks como yes por padrão, o 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 como no, por isso os espaços colocados antes de uma tag {% são preservados e aparecem no ficheiro renderizado. Se o resultado tiver indentação inesperada, defina lstrip_blocks: true na tarefa do template.

{{ ansible_managed }} é renderizado como o texto literal Ansible managed por padrão. Mantenha esse comportamento. É comum redefinir ansible_managed em ansible.cfg para incluir uma data. Nesse momento, o ficheiro renderizado fica diferente em todas as execuções, a tarefa indica uma alteração em todas as execuções e o serviço é recarregado em todas as execuções. Essa definição elimina a propriedade de que trata o resto deste guia. A extensão .j2 é uma convenção, e o Ansible não a verifica.

O playbook

Guarde este conteúdo 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 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 analisador YAML interpreta 0644 como um número simples, e pode acabar por aplicar permissões que não pretendia.

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 chega aos dois. Pode adicionar mais tarde um terceiro handler com a mesma linha listen, sem precisar de editar a tarefa do template. cache_valid_time: 3600 impede que uma segunda execução durante a mesma hora volte a contactar os repositórios de pacotes.

Execute uma vez e leia o que foi impresso

ansible-playbook site.yml

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

Leia primeiro as linhas de cada tarefa e, depois, o PLAY RECAP no final. Cada tarefa imprime 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. Depois de todas as tarefas do play terminarem, e não antes disso, 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 output:

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

nginx -t imprime 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 termos simples: connect() failed (111: Connection refused) while connecting to upstream.

Execute uma segunda vez para comprovar a idempotência

ansible-playbook site.yml

Esta é a execução que importa. Por isso, compare a saída com a primeira, linha a linha. A tarefa de template agora deve imprimir ok: onde antes imprimia changed:, e nenhum dos handlers deve aparecer em qualquer ponto da saída.

O mecanismo é simples e vale a pena conhecê-lo, porque é contra ele que você fará a depuração. template renderiza o arquivo no controller e compara o checksum do resultado com o checksum do arquivo que já está em dest. Conteúdo, proprietário e modo correspondentes significam que não há nada a fazer. Por isso, a tarefa informa 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 o play novamente, e a tarefa de template informará changed. Os dois handlers serão executados, e sudo cat /etc/nginx/conf.d/learn.conf exibirá o novo valor.

Se uma segunda execução idêntica ainda informar uma alteração, a renderização não é estável. Primeiro, procure algo baseado no tempo na saída, porque essa é a causa mais comum e um ansible_managed personalizado é o suspeito habitual. Depois, verifique se mode e owner na tarefa correspondem ao que está realmente no disco, porque uma divergência nesses valores representa 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. Para template, isto corresponde a uma diferença linha a linha entre o resultado renderizado e o ficheiro no disco. Em conjunto, 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 concluídas todas as tasks de um determinado play. 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."

O motivo é o agrupamento das alterações. Um play que gera quatro ficheiros de configuração para um serviço deve reiniciar esse serviço uma vez, no fim, com os quatro ficheiros já instalados. Reiniciá-lo depois de cada ficheiro faria o serviço reiniciar quatro vezes, e três desses reinícios carregariam uma configuração incompleta. A mesma página declara esta garantia de forma explícita: "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 apresentada na instrução notify." Por isso, Test the nginx configuration aparece acima 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 o “Ansible executar quaisquer 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. Na 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 posteriormente no play, por predefinição o handler não é executado nesse host, o que pode deixar o host num estado inesperado.” Assim, um play que gera uma configuração e depois falha numa tarefa sem relação deixa o novo ficheiro no disco, mas mantém a configuração antiga carregada no serviço em execução. Substitua esse comportamento com --force-handlers na linha de comandos ou com force_handlers: true no play. A mesma opção existe como force_handlers = True em [defaults] dentro de 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 perdedor falha silenciosamente

A documentação define a regra: "Cada handler deve ter um nome globalmente exclusivo. 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 única lista global de handlers para todo o play. Assim, se duas roles definirem Restart nginx, fica um nome que resolve para apenas uma delas. A ordem de carregamento determina qual delas será usada, e não a role a partir da qual fez a notificação.

Teste essa regra antes de depender dela. Guarde o conteúdo seguinte 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 escrito primeiro, não o último carregado. Isso é o oposto do que essa frase prevê.

Vale a pena compreender a diferença, porque a regra documentada aplica-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 distintos. Um bloco posterior oculta um bloco anterior. Uma lista simples de handlers: num play é um único bloco. A pesquisa dentro de um bloco é feita de cima para baixo e para no primeiro nome correspondente. Assim, dentro de um ficheiro, a primeira definição é usada e a segunda fica inacessível. Entre roles, o ocultamento ocorre da forma descrita na documentação. Em qualquer dos casos, nunca consegue aceder a ambos, e não deve basear a configuração em nenhuma destas ordens.

Há duas formas simples de resolver o problema. Dê a cada nome de handler 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 o modo "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 desta sintaxe. Isto torna-se um problema real assim que começa a incluir roles que não escreveu.

Há mais uma regra na mesma página: "Evite colocar variáveis no nome do handler. Como os nomes dos handlers são processados por 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 todo o play falhar quando essa variável não está definida no momento em que o nome é processado pelo 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 render inválido

validate executa um comando no ficheiro renderizado antes de o Ansible o colocar no destino. 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 deve 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 diretamente duas regras. %s é obrigatório, e uma string de validação sem ele faz a tarefa falhar com validate must contain %s. Além disso, não existe uma shell, pelo que pipes, redirecionamento, expansão de glob 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 único ficheiro e avalia-o de acordo com as suas próprias regras. visudo -cf lê um ficheiro sudoers. sshd -t -f lê um sshd_config completo.

Por que validate não pode validar o ficheiro do nginx neste guia

Adicione validate: /usr/sbin/nginx -t -c %s à tarefa de 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 blocos events e http. O ficheiro que esta 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, por isso o nginx rejeita um ficheiro que está correto no local onde é efetivamente utilizado. O verificador recebeu um fragmento e tentou tratá-lo como uma configuração completa.

A solução prática é a que já existe no playbook. Instale o fragmento e valide a configuração montada num handler definido antes do handler de reload. Como os handlers são executados pela ordem em que são definidos, nginx -t verifica o /etc/nginx/nginx.conf real com o seu fragmento incluído, e uma falha nesse ponto interrompe a play antes de systemctl reload ser chamado. Tenha em conta o custo: o ficheiro inválido fica no disco quando essa validaçã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 ao lado deste, com o nome basename.PID.YYYY-MM-DD@HH:MM:SS~, deixando no diretório 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 dessas cópias.

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. Ela não é inofensiva num diretório incluído com um * simples, e no Debian e no Ubuntu /etc/nginx/nginx.conf inclui /etc/nginx/sites-enabled/* exatamente dessa forma. Faça o template para sites-enabled com backup: true e o nginx carregará a cópia como um segundo server block ativo. É por isso que esta play escreve em conf.d.

Executar o mesmo play contra hosts reais do inventário

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

Duas coisas mudam. become: true passa a precisar de uma palavra-passe de sudo em cada destino, a menos que tenha sudo sem palavra-passe nesses hosts. Nesse caso, adicione -K. Qualquer segredo presente nesse template, como uma palavra-passe de base de dados ou um token de API, não deve ficar em texto simples em vars: num ficheiro que vai submeter para o repositório. 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 de uma variável.

Quando o play crescer para incluir mais de um serviço, vars:, templates/ e handlers: já têm uma localização padrão própria. Movê-los para lá é precisamente o objetivo de separar um playbook de um role.

FAQ

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

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

Por que meu playbook informa changed em todas as execuções?

O texto renderizado não é estável entre as execuções. A causa mais comum é um timestamp na saída. Uma string ansible_managed personalizada que inclua uma data produz exatamente esse efeito. Depois, verifique mode e owner na tarefa. Se não corresponderem ao ficheiro já existente no disco, o Ansible corrige-os e informa uma alteração, mesmo que o conteúdo seja idêntico. Execute ansible-playbook site.yml --check --diff para saber qual dos dois é o responsável, 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 primeiro com Jinja2 no controller e depois envia o resultado. Assim, as variáveis e os loops são resolvidos antes de o ficheiro chegar ao host de destino. Use copy para um ficheiro que seja idêntico em bytes em todos os hosts. Use template para qualquer ficheiro que varie conforme o host. Ambos partilham as mesmas opções de ficheiro. Portanto, mode, owner, backup e validate funcionam da mesma forma nos dois.

Como faço um handler ser executado no meio de um play?

Adicione ansible.builtin.meta: flush_handlers como uma tarefa no ponto em que pretende executar os handlers. Ele aciona todos os handlers notificados até então e, 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, um 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 depois execute nginx -t contra a configuração montada num handler definido acima 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 o reload ser tentado. Defina backup: true na tarefa de template para que o ficheiro anterior continue disponível para ser reposto.

#ansible#jinja2#handlers#idempotence#automation