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

Ansible: playbook ou role, quando usar cada um

Entenda quando um playbook simples basta e quando criar uma role: estrutura de diretórios, ansible-galaxy init, chamadas e precedência de variáveis.

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

Ansible playbook vs role: qual é a diferença

Um playbook do Ansible é o ficheiro que executa com ansible-playbook. Ele associa um grupo de hosts ao trabalho que esses hosts têm de executar. Uma role do Ansible é um diretório com uma estrutura fixa que contém tasks, templates, handlers e variáveis predefinidas. Um playbook chama essa role pelo nome. A sintaxe das tasks é idêntica nos dois casos. Portanto, a questão não é o que pode expressar. A questão é a reutilização.

Comece com um playbook simples. Um único site.yml que contém uma lista de tasks: é a estrutura certa para a sua primeira automação. Essa estrutura continua adequada durante mais tempo do que a maioria das pessoas espera. Converta-o numa role quando o mesmo bloco de tasks tiver de ser executado para um segundo grupo de hosts ou quando o ficheiro ultrapassar aproximadamente 100 linhas e já não conseguir encontrar uma task percorrendo o ficheiro.

Se ainda não escreveu nenhum, comece com um primeiro playbook para um único VPS e volte quando ele começar a crescer.

Quando um playbook simples é a escolha certa

Um playbook simples é adequado quando o trabalho é executado uma vez, num único host ou quando ninguém mais o vai ler. Provisionar um único servidor de aplicações ou aplicar patches num servidor antes de uma janela de manutenção não justifica uma árvore de diretórios. Uma role acrescenta sete diretórios e uma camada de indireção. Se o único chamador for o playbook que está ao lado dela, essa indireção não traz benefícios e obriga a saltar entre ficheiros sempre que quiser consultar o que é realmente executado.

O playbook simples deixa de ser adequado num momento específico, e é fácil identificá-lo. Copia um bloco de tarefas para um segundo playbook. Essa cópia é o sinal. A partir daí, cada correção tem de ser feita duas vezes e, um dia, será feita apenas uma vez.

O que um diretório de role realmente contém

roles/common/
  defaults/main.yml
  vars/main.yml
  tasks/main.yml
  handlers/main.yml
  templates/99-hardening.conf.j2
  files/
  meta/main.yml
  • tasks/main.yml é o ponto de entrada. O Ansible executa este ficheiro quando a role é chamada, e todos os outros diretórios são opcionais.
  • defaults/main.yml contém as variáveis que se espera que o chamador substitua. É a fonte com menor prioridade no Ansible, por isso quase tudo o resto tem precedência.
  • vars/main.yml contém as variáveis que não se espera que o chamador substitua. Tem prioridade superior à do inventário, o que é uma decisão forte. Use-o raramente.
  • handlers/main.yml contém as tarefas acionadas por notify. Um handler é executado no fim da play, uma vez, independentemente do número de tarefas que o notificaram.
  • files/ contém ficheiros copiados literalmente pelo módulo copy, e templates/ contém templates Jinja2 processados pelo módulo template. Dentro de uma role, faça referência a ambos apenas pelo nome do ficheiro, sem o caminho, porque o Ansible pesquisa primeiro nos diretórios da própria role.
  • meta/main.yml declara as dependências da role e os metadados que o Ansible Galaxy lê.

A estrutura não é uma preferência de estilo. O Ansible pesquisa exatamente estes caminhos, por isso um template colocado em roles/common/template/ (singular) simplesmente nunca é encontrado.

Criar a role comum com ansible-galaxy init

mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles common

Esse comando grava toda a estrutura em roles/common, incluindo diretórios que não serão usados e ficheiros main.yml que contêm apenas ---. Elimine os que ficarem vazios. Um vars/main.yml vazio não causa problemas ao Ansible, mas dificulta perceber quais são os ficheiros relevantes da role.

Agora preencha os ficheiros que executam o trabalho. Comece pelos defaults, porque são a interface pública da role.

# roles/common/defaults/main.yml
---
common_packages:
  - ufw
  - fail2ban
  - unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"

Coloque "no" e "yes" entre aspas. O Ansible analisa YAML com PyYAML, que interpreta um no sem aspas como o booleano false. Assim, a linha de configuração gerada torna-se PermitRootLogin False e o sshd rejeita-a. As aspas mantêm o valor como uma string.

# roles/common/tasks/main.yml
---
- name: Install the base packages
  ansible.builtin.apt:
    name: "{{ common_packages }}"
    state: present
    update_cache: true
    cache_valid_time: 3600

- name: Create the admin group
  ansible.builtin.group:
    name: "{{ common_admin_group }}"
    state: present

- name: Install the sshd hardening drop-in
  ansible.builtin.template:
    src: 99-hardening.conf.j2
    dest: /etc/ssh/sshd_config.d/99-hardening.conf
    owner: root
    group: root
    mode: "0644"
    validate: /usr/sbin/sshd -t -f %s
  notify: Restart sshd
# roles/common/handlers/main.yml
---
- name: Restart sshd
  ansible.builtin.service:
    name: ssh
    state: restarted
# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}

No Debian e no Ubuntu, a unidade systemd chama-se ssh. Nos sistemas da família RHEL, chama-se sshd. Um handler que indique a unidade errada só falha quando algo altera efetivamente o template. Por isso, o problema costuma aparecer semanas mais tarde.

A linha validate é a parte mais útil dessa task. O Ansible gera o template num ficheiro temporário, substitui %s pelo caminho desse ficheiro e executa o comando. O destino só é substituído se o comando terminar com o código 0. Coloque uma diretiva inválida no template e execute novamente. A task falha com failed to validate, o /etc/ssh/sshd_config.d/99-hardening.conf real permanece intacto e ainda terá um servidor ao qual pode iniciar sessão. Tenha em atenção que a verificação testa mais do que a sintaxe. Se sshd -t não conseguir ler as host keys, termina com sshd: no hostkeys available -- exiting. e o Ansible comunica o mesmo failed to validate. Por isso, consulte msg do módulo antes de culpar o template.

Como um playbook chama uma role

# site.yml
---
- name: Base configuration for every server
  hosts: all
  become: true
  roles:
    - common
# inventory.ini
[local]
localhost ansible_connection=local
ansible-playbook -i inventory.ini site.yml

O play deve terminar com failed=0 no resumo da execução. Passe os parâmetros no ponto da chamada usando a forma expandida. É assim que uma role pode servir dois grupos de hosts:

  roles:
    - role: common
      common_admin_group: ops
      common_permit_root_login: prohibit-password

Existe uma regra de ordenação que surpreende quase toda a gente. Um play pode conter pre_tasks, roles, tasks e post_tasks, e o Ansible executa-os nessa ordem, independentemente da ordem em que foram escritos no ficheiro. Coloque tasks: acima de roles: e as roles continuam a ser executadas primeiro. Por isso, se algo tiver de acontecer antes de uma role, deve ficar em pre_tasks:, não no início de tasks:.

- name: Ordering demonstration
  hosts: local
  gather_facts: false
  pre_tasks:
    - name: Runs first
      ansible.builtin.debug:
        msg: pre
  roles:
    - common
  tasks:
    - name: Runs after the role
      ansible.builtin.debug:
        msg: task
  post_tasks:
    - name: Runs last
      ansible.builtin.debug:
        msg: post

Para chamar uma role a partir de uma lista de tarefas, em vez de usar a chave roles:, utilize import_role ou include_role.

  tasks:
    - name: Static, read when the playbook is parsed
      ansible.builtin.import_role:
        name: common

    - name: Dynamic, resolved when the task runs
      ansible.builtin.include_role:
        name: postgres
      when: "'db' in group_names"

import_role é estático. O Ansible lê a role no momento da análise, e as respetivas tarefas passam a fazer parte do play. Por isso, ansible-playbook --list-tasks site.yml apresenta essas tarefas, e uma tag na importação aplica-se a todas as tarefas no interior. include_role é dinâmico. Nada é lido até a tarefa ser executada. Isto permite obter o nome da role a partir de uma variável ou de um loop. A desvantagem é que essas tarefas ficam invisíveis para --list-tasks e --start-at-task.

Existe uma armadilha neste caso. Um when: numa tarefa include_role é avaliado antes de defaults/main.yml da role incluída estar disponível. Se escrever when: common_packages | length > 0 na inclusão, a execução termina com 'common_packages' is undefined, mesmo que essa variável esteja definida na própria role que está a incluir. A correção é retirar essa opção da role: coloque-a em group_vars/all.yml, onde fica disponível em qualquer contexto, e deixe os valores predefinidos da role para as opções que a própria role utiliza.

Qual variável prevalece: defaults, group_vars, vars, extra vars

A documentação do Ansible descreve mais de vinte níveis de precedência de variáveis. Quatro deles resolvem quase todas as discussões reais. Estes são apresentados do mais fraco para o mais forte.

  • roles/<name>/defaults/main.yml fica perto da base. Quase tudo o que definir noutro local terá precedência, razão pela qual é o local correto para as opções ajustáveis de uma role.
  • group_vars/ e host_vars/ ficam no meio. É aqui que devem ficar as definições específicas do seu ambiente. Elas substituem claramente os valores predefinidos da role.
  • roles/<name>/vars/main.yml tem precedência sobre host_vars. Um valor definido aqui não pode ser substituído pelo inventário. Reserve-o para elementos que a role precisa de manter consistentes internamente, como um nome de pacote que tenha de corresponder ao nome de um serviço.
  • Um parâmetro de role passado no ponto de chamada tem precedência sobre vars/main.yml, e -e na linha de comandos tem precedência sobre tudo, incluindo os parâmetros da role.

Pode observar esta resolução em cerca de um minuto. Crie uma role pequena com um valor predefinido e uma variável de role. Depois, defina os mesmos nomes em host_vars.

# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults
# roles/prec/vars/main.yml
---
prec_internal: from-rolevars
# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars
# roles/prec/tasks/main.yml
---
- name: Show which value survived
  ansible.builtin.debug:
    msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"
ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cli

A primeira execução apresenta tunable=from-hostvars internal=from-rolevars. O inventário teve precedência sobre o valor predefinido da role, mas perdeu para a variável da role. A segunda execução apresenta internal=from-cli, porque as variáveis extra ficam no nível mais alto e nada abaixo delas pode substituí-las. É também por isso que -e é adequado para uma execução pontual, mas é uma escolha errada num script que mantém: tem silenciosamente precedência sobre todas as decisões consideradas no seu repositório.

A regra prática é: se quiser que um valor possa ser definido, coloque-o em defaults/. Colocá-lo em vars/ informa todos os utilizadores futuros da role de que o inventário não o pode alterar. Por vezes é isso que pretendia. Na maioria dos casos, é um acidente.

Demonstre a idempotência da role: execute-a duas vezes

Uma execução do Ansible em que se pode confiar produz o mesmo resultado na segunda vez e informa que nada foi alterado. Execute o playbook duas vezes e leia o resumo.

ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml

O segundo resumo deve ser semelhante a este:

PLAY RECAP *********************************************************************
localhost   : ok=4  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0 significa que cada módulo inspecionou o estado atual e constatou que o trabalho já estava concluído. changed=2 numa segunda execução significa que duas tarefas não conseguem detetar a diferença. Por isso, continuarão a reescrever ficheiros e a reiniciar serviços indefinidamente. A causa habitual é command ou shell, porque o Ansible não tem como saber o que um comando arbitrário fez.

# traps.yml
---
- name: Command modules do not know what they changed
  hosts: local
  gather_facts: false
  tasks:
    - name: This appends a line on every run
      ansible.builtin.shell: "echo run >> /tmp/grow.txt"

    - name: This appends a line only once
      ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
      args:
        creates: /tmp/guarded.txt

Execute esse playbook duas vezes e conte as linhas com wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt contém duas linhas e /tmp/guarded.txt contém uma. Na segunda execução, a tarefa condicionada não foi executada e o resultado contém a mensagem skipped, since /tmp/guarded.txt exists, porque creates fornece ao módulo um resultado visível que pode ser procurado primeiro. Quando um comando não deixa esse resultado, registe a saída e tome a decisão manualmente com changed_when.

ansible-playbook --check --diff site.yml prevê alterações sem as aplicar, e --diff mostra as linhas exatas que um template reescreveria. Leia a saída tendo uma ressalva em mente: as tarefas shell e command são ignoradas no modo de verificação, portanto um plano que parece limpo ainda pode ocultar trabalho.

Outra coluna desse resumo exige a mesma atenção: um host ao qual o Ansible não conseguiu ligar é contado em unreachable, e não em failed. Nenhuma das tarefas desse host foi executada. Por isso, decida antecipadamente se um host inacessível deve interromper toda a execução antes de aplicar esta role a mais de algumas máquinas.

Por que o Ansible informa que a role não foi encontrada

O Ansible procura um diretório roles/ ao lado do ficheiro do playbook e, depois, em roles_path. A pesquisa segue o playbook, não a shell.

ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely

Essa mensagem significa que site.yml e roles/ deixaram de estar alinhados. A mensagem também mostra os caminhos que foram testados. Mantenha os dois no mesmo diretório. Executar a partir de um diretório-pai funciona, porque o que determina a pesquisa é o caminho do playbook:

ansible-playbook -i infra/inventory.ini infra/site.yml

Existe uma versão mais silenciosa do mesmo problema. O Ansible ignora um ansible.cfg no diretório atual quando esse diretório permite escrita a qualquer utilizador, porque qualquer utilizador do sistema poderia colocar ali uma configuração e alterar o comportamento da execução.

[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.

Nesse caso, as definições roles_path e inventory ficam silenciosamente ausentes, e a pesquisa da role falha por um motivo que não está relacionado com roles. ansible --version mostra os config file que foram efetivamente carregados, e ansible-config dump --only-changed mostra todas as definições diferentes dos valores predefinidos. Consulte ambos sempre que uma execução se comportar como se a sua configuração não existisse.

Partilhar roles: requirements.yml e uma versão fixada

Uma role escrita por outra pessoa é instalada, não copiada. Declare-a uma vez:

# requirements.yml
---
roles:
  - name: postgres
    src: https://github.com/example/ansible-role-postgres
    scm: git
    version: v1.4.0
ansible-galaxy install -r requirements.yml -p galaxy_roles

Defina sempre version. Sem essa definição, é usada a branch predefinida no dia em que o comando é executado. Assim, uma implementação que funcionava no mês passado pode falhar sem qualquer alteração no seu próprio repositório. Aponte roles_path para o diretório de download e mantenha esse diretório fora do git:

# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_roles

As roles em roles/ junto do playbook continuam a ser encontradas, porque esse caminho é sempre pesquisado além de roles_path. Dessa forma, as suas próprias roles permanecem versionadas e revistas, enquanto as roles de terceiros são downloads reproduzíveis fixados numa tag.

Quando as roles deixam de ser a resposta

Uma role é uma unidade de reutilização dentro de uma execução do Ansible. Ela não cria servidores nem registos DNS no seu provedor. Tentar fazê-la assumir essa função é uma forma de transformar os playbooks em algo que ninguém quer manter. Vale a pena ler a separação de responsabilidades entre Ansible e Terraform antes de começar. Uma role também não substitui o desenho do inventário. Quando passa a gerir mais do que algumas máquinas, a forma como agrupa e acede a esses servidores é mais importante do que a forma como as tarefas estão organizadas.

O hardening instalado por esta role common também exige decisões próprias. O drop-in acima define apenas duas diretivas. Por isso, leia quais definições do SSH vale realmente a pena alterar e como fazer o Ubuntu aplicar automaticamente as atualizações de segurança antes de decidir o que deve pertencer à role em todos os hosts que administra.

FAQ

Quando devo transformar um playbook do Ansible numa role?

Quando o mesmo bloco de tarefas tiver de ser executado num segundo play ou contra um segundo grupo de hosts. Copiar tarefas entre playbooks é o sinal, porque, a partir desse momento, cada correção tem de ser aplicada duas vezes e, um dia, será aplicada apenas uma vez. Um único playbook com cerca de 100 linhas, que se destina sempre a um só grupo, não ganha nada com uma role, e os diretórios adicionais tornam a leitura mais difícil.

As roles são executadas antes das tarefas no mesmo play?

Sim. O Ansible executa pre_tasks, depois tudo o que estiver listado em roles:, depois tasks: e, por fim, post_tasks:. A ordem em que essas chaves aparecem no ficheiro é ignorada. Escrever tasks: acima de roles: não faz com que essas tarefas sejam executadas primeiro. Se algo tiver de acontecer antes de uma role, coloque-o em pre_tasks:.

Porque é que o valor em group_vars não substitui o valor da role?

Verifique se a variável está definida em vars/main.yml da role, em vez de estar em defaults/main.yml. vars/ tem precedência sobre group_vars e host_vars na ordem de precedência do Ansible, pelo que o inventário não o pode substituir. Mova a variável para defaults/main.yml, que fica perto do nível inferior da ordem e é o local correto para qualquer valor que o chamador deva poder alterar. Para confirmar que a causa é a precedência e não um erro de escrita, execute uma vez com -e name=value, que tem precedência sobre qualquer outra origem.

Porque é que o Ansible diz que a role não foi encontrada?

A pesquisa começa junto ao ficheiro do playbook, pelo que site.yml e roles/ têm de estar no mesmo diretório. O erro apresenta os caminhos que tentou, como em the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. Executar o playbook a partir de um diretório pai não é um problema, porque a pesquisa segue o caminho do playbook e não o diretório de trabalho da shell. Se depender de roles_path de ansible.cfg, confirme que esse ficheiro foi carregado com ansible --version, porque um diretório de trabalho com permissões de escrita para todos faz com que o Ansible o ignore.

Preciso de executar ansible-galaxy init para criar uma role?

Não. Uma role é apenas um conjunto de diretórios com os nomes esperados, pelo que mkdir -p roles/common/tasks e um tasks/main.yml já formam uma role funcional. ansible-galaxy init --init-path roles common evita escrever tudo manualmente e cria o esqueleto completo, incluindo meta/main.yml e um README básico. Elimine os diretórios que ficarem vazios, porque um vars/main.yml vazio esconde quais ficheiros da role executam efetivamente alguma ação.

#ansible#roles#playbook#structure#automation