ANSIBLE SERIES: h.t.wrt* … tasks, plays e books: building loops

CONT.

👈 ANTERIOR: BECOME

O que fazer quando é necessário repetir determinada ação mais de uma vez ??? Automatizar, é claro, você diria … Tudo bem, já entendi que pegou o espírito da coisa 🙃 E fico deveras feliz por isso, diga-se de passagem! Mas, o que eu quis dizer é como fazê-lo em pormenores? Como executar valendo-se da lógica da programação? Há duas respostas válidas, contudo uma é mais inteligente e menos trabalhosa, e a outra é mais lenta (para não desmerecê-la 😬 haha) e braçal. A primeira delas seria utilizar um monte de IF’s e ELSE’s para cada entrada, interação ou condição. Em contrapartida, a segunda seria criar um laço (daí seu nome loop, em inglês) com uma variável “contador” interna. Essa controlaria a execução do mesmo em N vezes, e até que a condição X fosse satisfeita. Resultado? Um código mais bonito, limpo, enxuto, com menos linhas, e mais otimizado.

No contexto do Ansible, ou de qualquer outra ferramenta de automação, tais ações “repetitivas” abrangeriam desde adicionar vários usuários, replicar uma configuração em um pool de máquinas, até mudar a propriedade/permissão (ou dono) de uma série de arquivos e pastas, por exemplo. Para tal o ansible nos fornece duas palavras-chave: loop e with_<lookup> (lookup em português pode ser traduzido como busca, pesquisa, chave ou valor).

Antes de expor e debater os tópicos a seguir, cabe aqui, e quero destacar algumas notas acerca do recurso loop :

  • Foi introduzido a partir do Ansible 2.5
  • Está presente em todas as versões sucessoras (2.5+)
  • Ainda não substitui completamente o with_<lookup>, embora a ideia seja exatamente essa no futuro.
  • Na maioria dos casos recomenda-se, e satisfaz muito bem, o uso do loop . Somente em situações extremamente específicas o with_<lookup> dá pleno suporte, o que acaba “vencendo” seu concorrente amigo, e portanto o uso dele é o único meio viável para solucionar.
  • Não está obsoleta ou tão pouco depreciada a sintaxe do with_lookup, continuando assim totalmente válida, embora isso possa mudar no médio-longo prazo.
  • O loop encontra-se em constante mudança visando principalmente seu aperfeiçoamento. Para atualizações e alterações do mesmo, por favor consultar o CHANGELOG https://github.com/ansible/ansible/tree/devel/changelogs

02-a. Comparativo entre loop e with_*

As únicas palavras-chave capazes de complementar o with_ são aquelas que estão disponíveis em https://docs.ansible.com/ansible/latest/plugins/lookup.html#lookup-plugins Ou seja, na prática, qualquer uma delas quando utilizadas correspondem ao asterisco (presente no título desta sessão). Para refrescar a memória, * é conhecido também como o caractere-curinga na computação, especificamente quando falamos em interpretadores de comandos nos sistemas operacionais.

A palavra loop é o mesmo que usar a expressão with_list, sendo a primeira uma melhor escolha para loops que não possuem tanta complexidade. Porém, o loop não aceita como entrada nenhuma ‘string’. Veja mais em https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#query-vs-lookup

Genericamente, e simplificando bastante, qualquer with_* pode facilmente ser trocado por loop. Claro que salva as devidas proporções e ressalvas, todas essas descritas aqui https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#migrating-to-loop

02-b. Loops Tradicionais (ou padrão/modelos)

! ! ! Interagindo sobre uma lista simples ! ! !

Se uma task precisa ser executada mais de uma vez em determinado host, recomenda-se usar como padrão o próprio loop, assim este agiria de acordo e sobre uma lista de strings, por exemplo. Em outras palavras, defina a lista diretamente, de uma vez só e já estando dentro da task em questão:

- name: Add users and give them root powers
  ansible.builtin.user:
    name: "{{ item }}"
    state: present
    group: "wheel"
  loop:
     - victor
     - viktor
     - vitor
     - vicrlda

O código acima ☝ seria equivalente ao de baixo 👇 (só que mais elegante! e com menos tec-tec-tec-tec 🔉💻💬)

- name: Add user vitor
  ansible.builtin.user:
    name: "vitor"
    state: present
    groups: "wheel"

- name: Add user vicrlda
  ansible.builtin.user:
    name: "vicrlda"
    state: present
    groups: "wheel"

.
.

.
.

! ! ! Interagindo sobre uma lista de hashes ! ! !

Caso tenha uma lista de hashes para trabalhar, utilize referências no formato de chave:subchave dentro do loop.

- name: Add multiple users
  ansible.builtin.user:
    name: "{{ item.name }}"
    state: present
    groups: "{{ item.groups }}"
  loop:
    - { name: 'victor', groups: 'wheel' }
    - { name: 'viktor', groups: 'root' }

! ! ! Interagindo sobre um dictionary ! ! !

Neste caso, simplesmente use o dict2items https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#dict-filter :

- name: Using dict2items
  ansible.builtin.debug:
    msg: "{{ item.key }} - {{ item.value }}"
  loop: "{{ tag_data | dict2items }}"
  vars:
    tag_data:
      Environment: dev
      Application: payment

02-c. Registrando variáveis com um loop

Esse artifício é comumente usado na lógica da programação, em termos genéricos, e desde sempre, remontando ao seu início e concepção. Naturalmente, por tabela, também é observado nas linguagens. Em outras palavras, o que temos é, basicamente um evento partilhado ocorrendo tanto no macro quanto no micro ecossistema. Respeitando, é claro, as particularidades de cada um (limitações, capacidades e sintaxe).

E com os playbooks não poderia ser diferente. Somos capazes de armazenar a saída de um loop dentro de uma variável. Por exemplo,

- name: Register loop output as a variable
  ansible.builtin.shell: "echo {{ item }}"
  loop:
    - "apple"
    - "banana"
  register: echo

Loops que vem logo após a variável registrada, com o intuito de inspecionar eventuais resultados, talvez se assemelhem ao trecho a seguir:

- name: Fail if return code is not 0
  ansible.builtin.fail:
    msg: "The command ({{ item.cmd }}) did not have a 0 return code"
  when: item.rc != 0
  loop: "{{ echo.results }}"

Por padrão, o comportamento da variável durante a execução/interação do loop é sempre armazenar o valor do item atual, indiferente se são números, strings ou caracteres.

- name: Place the result of the current item in the variable
  ansible.builtin.shell: echo "{{ item }}"
  loop:
    - one
    - two
  register: echo
  changed_when: echo.stdout != "one"

02-d. Loops Complexos

! ! ! LISTAS ANINHADAS ! ! !

Aninhado(a), conceitualmente restrito ao universo da programação, implica dizer que um dado elemento está contido em outro elemento. Se preferir mais sinônimos, seria o mesmo que alojado, hospedado, instalado. Isso compreende desde funções, sub-rotinas, até vetores e matrizes. Exemplo: um vetor de tamanho três [0, 1, 2] cujas posições na memória são na verdade ponteiros para outros três vetores distintos. Talvez o mais importante aqui, e que vale destacar, é o tamanho da jurisprudência do primeiro ou o limite imposto ao segundo … ❓❓❓ Certo, certo, eu explico. Vamos imaginar que os sujeitos (elementos) em questão sejam rotinas e sub-rotinas. Então, partindo do pressuposto que apresentei, uma sub-rotina “filha” somente poderia ser chamada para rodar durante o programa única e exclusivamente por sua rotina “mãe”. Caso uma rotina “madrasta”, por exemplo, tentasse invocar essa mesma sub-rotina “filha”, não haveria êxito algum. Isso porque, de antemão, a primeira (madrasta) nunca iria enxergar a segunda (filha) pois ela apenas fica visível para a terceira (mãe). Isso é o que determinados autores chamam de “fazer sentido localmente”, e que é considerada uma forma de encapsulamento.

Ufa! 🥴 Agora sim, voltando ao contexto dos playbooks, e finalizando esse tópico antes de passar para o próximo (…)

Em situações de listas aninhadas recomenda-se o uso de expressões Jinja2 para melhor tratá-las. Como segue abaixo, um loop que combina várias listas:

- name: Give users access to multiple databases
  community.mysql.mysql_user:
    name: "{{ item[0] }}"
    priv: "{{ item[1] }}.*:ALL"
    append_privs: yes
    password: "foo"
  loop: "{{ ['alice', 'bob'] |product(['clientdb', 'employeedb', 'providerdb'])|list }}"

! ! ! INVENTÁRIOS ! ! !

Mais uma opção para uso dos loops no Ansible ocorre quando se trata de arquivos de inventário. Isso nos possibilita varrê-los ou construí-los sem sair do laço. E melhor, não é obrigatório usar todo o arquivo e sim apenas uma parte dele, caso prefira assim. Para tal, basta informar a palavra-chave loop com a variável ansible_play_batch ou a variável groups

- name: Show all the hosts in the inventory
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ groups['all'] }}"

- name: Show all the hosts in the current play
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ ansible_play_batch }}"

02-e. Adicionando controles aos loops

(A partir da versão 2.1+)

(Palavra-chave: loop_control)

! ! ! LIMITANDO A TELA/SAÍDA DO LOOP ! ! !

Para estruturas de dados muito complexas, executar um laço pode ser algo extenso, e consequentemente, a saída gerada (console) se torna igualmente enorme. Evite dores de cabeça e olhos cansados utilizando a diretriz label com loop_control :

- name: Create servers
  digital_ocean:
    name: "{{ item.name }}"
    state: present
  loop:
    - name: server1
      disks: 3gb
      ram: 15Gb
      network:
        nic01: 100Gb
        nic02: 10Gb
        ...
  loop_control:
    label: "{{ item.name }}"

! ! ! Pausa dentro de um loop ! ! !

Controle o tempo, aqui medido em segundos, entre a execução de cada item dentro de uma loop task (tarefa repetitiva). Use a diretriz pause com a palavra loop_control

# main.yml
- name: Create servers, pause 3s before creating next
  community.digitalocean.digital_ocean:
    name: "{{ item }}"
    state: present
  loop:
    - server1
    - server2
  loop_control:
    pause: 3

! ! ! Definindo nomes internos e externos ao loop com loop_var ! ! !

Você pode aninhar duas tarefas de loop usando include_tasks. No entanto, por padrão, Ansible define o item de variável de loop para cada loop. Isso significa que o loop interno aninhado sobrescreverá o valor do item do loop externo. Você pode especificar o nome da variável para cada loop usando loop_var com loop_control:

# main.yml
- include_tasks: inner.yml
  loop:
    - 1
     - 2
     - 3
  loop_control:
    loop_var: outer_item

# inner.yml
- name: Print outer and inner items
  ansible.builtin.debug:
    msg: "outer item={{ outer_item }} inner item={{ item }}"
  loop:
    - a
     - b
     - c

CONTINUA (…)

Próximo post >>> WHERE TASKS RUN?

REFERÊNCIAS:

https://docs.ansible.com/core.html

https://docs.ansible.com/ansible/latest/user_guide/index.html

https://docs.ansible.com/ansible/latest/user_guide/index.html#writing-tasks-plays-and-playbooks

https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#playbooks-loops

Deixe uma resposta

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.