SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-07

Ansible: playbook ou role, qual usar e quando

Veja quando um playbook simples basta e quando usar uma role, com 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 precisa de realizar. 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, não se trata do que pode expressar. Trata-se da reutilização.

Comece com um playbook simples. Um único site.yml que contém uma lista 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.

Se ainda não escreveu um, comece com um primeiro playbook para um único VPS e volte quando 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 correções num servidor antes de uma janela de manutenção não justifica uma árvore de diretórios. Uma role adiciona sete diretórios e uma camada de indireção. Se o único consumidor for o playbook que está ao lado dela, essa indireção não traz benefícios e obriga-o a saltar de ficheiro sempre que quiser ver o que é realmente executado.

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

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

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 sobre ela.
  • vars/main.yml contém as variáveis que não se espera que o chamador substitua. Fica acima do inventário na ordem de prioridade, o que é uma decisão importante. 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 os diretórios da própria role.
  • meta/main.yml declara as dependências da role e os metadados lidos pelo Ansible Galaxy.

A estrutura não é uma preferência de estilo. O Ansible pesquisa estes caminhos exatos, 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 todo o esqueleto em roles/common, incluindo diretórios que você não usará e stubs main.yml que contêm apenas ---. Exclua os que permanecerem vazios. Um vars/main.yml vazio não causa problemas para o Ansible, mas oculta quais arquivos da role realmente importam.

Agora preencha os arquivos que executam o trabalho. Comece pelos defaults, porque eles 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. Por isso, a linha de configuração renderizada se torna PermitRootLogin False, e o sshd a rejeita. As aspas mantêm o valor como 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 do systemd se chama ssh. Nos sistemas da família RHEL, ela se chama sshd. Um handler que nomeia a unidade errada só falha quando algo realmente altera o template. Por isso, o problema geralmente só aparece semanas depois.

A linha validate é a parte mais útil dessa task. O Ansible renderiza o template em um arquivo temporário, substitui %s pelo caminho desse arquivo 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 você ainda tem um servidor ao qual pode se conectar. Tenha em mente que a verificação testa mais do que a sintaxe. Se sshd -t não conseguir ler as host keys, ele terminará com sshd: no hostkeys available -- exiting., e o Ansible informará o mesmo failed to validate. Por isso, consulte msg do módulo antes de atribuir o problema ao 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 atende dois grupos de hosts:

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

Há 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:, use 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 lista essas tarefas e uma tag na importação aplica-se a todas as tarefas internas. include_role é dinâmico. Nada é lido até a tarefa ser executada. Isso permite definir 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.

Há uma armadilha neste ponto. Um when: numa tarefa include_role é avaliado antes de defaults/main.yml da role incluída ficar no escopo. Se escrever when: common_packages | length > 0 na inclusão, a execução para 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 no escopo em todo o lado, e deixe os defaults da role para os valores que a própria role consome.

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

O Ansible documenta mais de vinte níveis de precedência de variáveis. Quatro deles resolvem quase todas as discussões reais. Aqui estão da menor para a maior precedência.

  • roles/<name>/defaults/main.yml fica perto do nível inferior. Quase tudo o que for definido noutro local tem precedência sobre ele. Por isso, é o local correto para os parâmetros ajustáveis de uma role.
  • group_vars/ e host_vars/ ficam no meio. É aqui que devem ficar as respostas específicas do seu site. Elas substituem claramente os valores padrão 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 tem de corresponder a um nome de serviço.
  • Um parâmetro de role passado no ponto de chamada tem precedência sobre vars/main.yml. Já -e, definido na linha de comandos, tem precedência sobre tudo, incluindo os parâmetros da role.

Pode observar este processo em cerca de 1 minuto. Dê a uma role pequena um valor padrão e uma variável da 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 imprime tunable=from-hostvars internal=from-rolevars. O inventário teve precedência sobre o valor padrão da role, mas perdeu para a variável da role. A segunda execução imprime internal=from-cli, porque as variáveis adicionais 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 está errado num script mantido a longo prazo: ele ultrapassa silenciosamente todas as decisões consideradas no seu repositório.

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

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

Uma execução do Ansible em que 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 concluiu que o trabalho já estava feito. changed=2 numa segunda execução significa que duas tarefas não conseguem distinguir o estado atual do estado pretendido. 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 protegida não foi executada e o respetivo resultado contém a mensagem skipped, since /tmp/guarded.txt exists, porque creates fornece ao módulo um produto visível para procurar primeiro. Quando um comando não deixa esse produto, registe a respetiva saída e tome a decisão com changed_when.

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

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

O Ansible procura um diretório roles/ junto ao ficheiro do playbook e, em seguida, em roles_path. A pesquisa segue o playbook, não a sua 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 tentados. Mantenha os dois no mesmo diretório. Executar a partir de um diretório-pai não é um problema, porque o caminho do playbook é o que conta:

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

Existe uma variante mais silenciosa do mesmo problema. O Ansible ignora um ansible.cfg no diretório atual quando esse diretório permite escrita a todos os utilizadores, 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 configuraçõ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 o config file que foi efetivamente carregado, e ansible-config dump --only-changed mostra todas as configurações que diferem 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. Assim, as suas próprias roles permanecem versionadas e revistas, enquanto as roles de terceiros são downloads reproduzíveis fixados numa tag.

Quando os roles deixam de ser a resposta

Um role é uma unidade de reutilização dentro de uma execução do Ansible. Não cria servidores nem registos DNS no seu fornecedor, e tentar fazê-lo é uma forma de transformar os playbooks em algo que ninguém quer manter. Vale a pena ler a divisão do trabalho entre Ansible e Terraform antes de começar. Um role também não substitui o desenho do inventário: depois de ultrapassar 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 reforço de segurança que este role common instala também exige decisões próprias. O drop-in acima define apenas duas diretivas. Por isso, leia quais as definições de SSH que 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 ao role em todos os hosts que gere.

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 aproximadamente menos de 100 linhas, que seja sempre destinado a um só grupo, não beneficia de uma role, e os diretórios adicionais tornam a leitura mais difícil.

As roles são executadas antes das tarefas do 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:.

Por que motivo 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 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 está perto do fim 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 digitação, execute uma vez com -e name=value, que tem precedência sobre todas as outras fontes.

Por que motivo 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 foram tentados, 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 a partir 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 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á constituem uma role funcional. ansible-galaxy init --init-path roles common evita a introdução manual e cria o esqueleto completo, incluindo meta/main.yml e um ficheiro README inicial. Elimine os diretórios que ficam vazios, porque um vars/main.yml vazio dificulta perceber quais são os ficheiros da role que realmente fazem alguma coisa.

#ansible#roles#playbook#structure#automation