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.
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.ymltasks/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.ymlconté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.ymlconté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.ymlcontém as tarefas acionadas pornotify. 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ódulocopy, etemplates/contém templates Jinja2 processados pelo módulotemplate. 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.ymldeclara 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 commonEsse 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=localansible-playbook -i inventory.ini site.ymlO 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-passwordExiste 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: postPara 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.ymlfica 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/ehost_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.ymltem precedência sobrehost_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-ena 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-cliA 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.ymlO segundo resumo deve ser semelhante a este:
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=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.txtExecute 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/lonelyEssa 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.ymlExiste 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.0ansible-galaxy install -r requirements.yml -p galaxy_rolesDefina 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_rolesAs 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.