Voltar para artigos

Ansible no mundo real: do playbook ao botão na plataforma de automação

Como estruturar, testar e colocar em produção playbooks de automação.

Como estruturar, testar e colocar em produção playbooks de automação — com lições de uma operação real de patch management, orquestração de serviços e integrações com Zabbix, Jira e Automation Platform.

Artigo técnico • ~15 min de leitura


Introdução

Escrever um playbook Ansible é fácil. Escrever um playbook que sobrevive ao dia a dia de uma operação de infraestrutura — com janelas de manutenção, ambientes de homologação/produção, exceções por família de servidor, auditoria e zero downtime — é outra conversa.

Neste artigo, vamos dissecar um acervo real de mais de 100 playbooks usados em produção por uma equipe de operações, cobrindo desde rotinas de patch management até orquestração de start/stop de aplicações Java legadas. A ideia é transformar esse conhecimento em um guia prático sobre:

  1. A lógica básica de desenvolvimento de playbooks (as decisões que se repetem em 90% dos casos);
  2. Estruturas de testes que dão coragem para executar na produção;
  3. O caminho de implantação em produção via plataforma de automação (AAP/AWX) com surveys, workflows e RBAC;
  4. Casos de uso interessantes que resolvem dores reais de operação.

Nota: todos os exemplos foram anonimizados — hostnames, domínios e endpoints foram trocados por nomes genéricos. A estrutura dos playbooks, porém, é fiel ao que roda em produção.


1. A lógica básica de desenvolvimento

Antes de qualquer framework de roles ou collections sofisticadas, existem padrões simples que fazem um playbook ser previsível, reutilizável e seguro. Veja os seis pilares que se repetem em toda a base analisada.

1.1 O padrão "playbook genérico + alvo dinâmico"

O primeiro pilar é o mais simples: um único playbook atende a múltiplos alvos, definidos em tempo de execução. Isso transforma o playbook em um recurso que a plataforma pode reaproveitar:

---
- name: Executar yum update
  hosts: "{{ servidores }}"
  gather_facts: false

  vars:
    # Valor padrão caso 'servidores' não seja fornecido
    servidores: all

A variável servidores chega por survey (formulário da plataforma) ou extra-vars, e o valor padrão (all) evita que o job quebre por variável ausente. O time consegue, com um único job template, atualizar "todos os lab", "só a fila de prioritários" ou "um host específico".

Por que isso importa: em vez de 30 playbooks quase iguais, você tem 1 playbook + 30 combinações de entrada. Menos código duplicado, menos superfície de erro.

1.2 gather_facts: false e coleta seletiva de facts

Coletar todos os facts do setup em centenas de hosts é desperdício. Quase todo playbook da base começa com gather_facts: false e, quando precisa de algo específico, chama o módulo setup pedindo apenas o subset necessário:

- name: Setup (gather limited facts)
  ansible.builtin.setup:
    gather_subset:
      - '!all,!min'
      - 'network'
      - 'date_time'

Esse padrão aparece para capturar apenas facts de rede e data/hora (usados, por exemplo, para gerar sufixos com timestamp). O ganho de tempo é imediato — e a superfície de informação sensível coletada diminui, o que ajuda em auditorias.

1.3 Variáveis em arquivos, lógica no playbook

Outra regra recorrente: listas e valores de "conhecimento operacional" vivem em vars_files, nunca embutidos na lógica:

  vars_files:
    - vars/playbook_vars_inventarios.yml       # listas de hosts prioritários, grupos OTDM, etc.
    - vars/playbook_vars_zabbix.yml            # versões e flags do agente Zabbix
    - vars/playbook_comandos_start_linux.yml   # comandos de start por serviço/ambiente

Isso separa o dado (muda com frequência) da lógica (muda raramente). Um playbook de inicialização de serviços NÃO contém a lista dos 20 serviços Java da produção — ele itera sobre a lista vinda do arquivo externo:

- name: Iniciando serviços PRODUÇÃO
  loop: "{{ comandos_prod }}"
  ansible.builtin.include_tasks:
    file: tasks/playbook_task_start_java.yml

Os tasks/ reutilizáveis (start_java, status_java, upload_nexus, download_internet) completam a arquitetura: playbook = orquestração, task file = unidade de trabalho, var file = fonte da verdade.

1.4 Blocos, quando e a divisão por ambiente

O condicional por ambiente (ambiente == 'PROD' | 'NAO_PROD' | 'LAB') aparece na forma de blocos paralelos por ambiente dentro do mesmo playbook:

- name: Ambiente PRODUÇÃO
  block:
    - name: Iniciando serviços PRODUÇÃO
      loop: "{{ comandos_prod }}"
      ansible.builtin.include_tasks:
        file: tasks/playbook_task_start_java.yml
  when: ambiente == 'PROD'

- name: Ambiente LAB
  block:
    - name: Iniciando serviços LAB
      loop: "{{ comandos_lab }}"
      ansible.builtin.include_tasks:
        file: tasks/playbook_task_start_java.yml
  when: ambiente == 'LAB'

Já no nível de host, o filtro é feito por padrão no próprio inventory_hostname:

- name: Atualizar pacotes dos servidores OpenLDAP
  ansible.builtin.yum:
    name: "*"
    state: latest
    exclude: openldap*
    disable_gpg_check: true
  register: yum_update_result
  when: "'xat' in inventory_hostname"

Esse padrão — "mesma tarefa, comportamento derivado de uma substring do hostname" — é um atalho pragmático usado em operação. É rápido de escrever, mas vale a régua: quando a regra começa a ter 3+ exceções, é hora de migrar para grupos de inventário (veremos que a base faz isso com listas externas).

1.5 Listas de exceção versionadas como código

Os "hosts prioritários", "hosts OpenLDAP", "hosts OTDM" não são grupos fixos no inventário — são listas em YAML versionado:

# playbook_reboot_v2.yml (adaptado)
- name: Reboot dos Hosts Não Prioritários
  ansible.builtin.reboot:
  when: prioridade == 'NÃO' and
        inventory_hostname not in otdm_homol + otdm_producao +
        prioritarios_homol + prioritarios_producao

- name: Reboot dos Hosts Prioritários
  ansible.builtin.reboot:
  when: prioridade == 'SIM' and
        inventory_hostname in prioritarios_homol + prioritarios_producao

Repare no detalhe: o survey decide (prioridade: SIM | NÃO) e as listas versionadas garantem que um reboot atinja o conjunto correto. Mudou a lista de prioritários? É um commit, não um edit manual no inventário. Esse é o espírito do GitOps infiltrado na operação.

1.6 Controle de resultado: changed_when, failed_when e ignore_errors

Comandos imperativos (shell, command) quebram o modelo declarativo do Ansible — a menos que você diga explicitamente o que é "mudança" e o que é "falha":

- name: Verificar PIDs Confluence
  ansible.builtin.shell: "jps -l | egrep 'Bootstrap|synchrony' | awk '{print $1}'"
  register: pids_conf
  changed_when: false      # consulta: nunca "muda" nada
  failed_when: false       # nenhum processo é um estado legítimo

- name: Matar PIDs zumbis
  ansible.builtin.shell: "kill -9 {{ item }}"
  loop: "{{ pids_conf.stdout_lines }}"
  when: pids_conf.stdout_lines | length > 0
  changed_when: true
  failed_when: false       # PID sumiu entre a checagem e o kill? segue o baile

E nos casos deliberadamente "best effort" (reboot em massa, stop de serviços para janela), a opção ignore_errors: yes com um comentário no cabeçalho explica por que o erro é tolerado — documentação vivendo junto do código.


2. Estruturas de testes

Playbook sem etapa de teste é roleta-russa. A base analisada revela uma estratégia de testes em camadas que vale descrever como receituário.

2.1 Camada 0 — Lint e estilo (CI barato e alto valor)

Todo YAML passa por um pipeline mínimo antes de qualquer execução:

# Sintaxe YAML estrutural
ansible-lint playbook_yum-update_v2.yml
yamllint playbook_yum-update_v2.yml

# Sintaxe Ansible (nomes de módulos, campos, templates)
ansible-playbook --syntax-check playbook_yum-update_v2.yml

O ansible-lint não é pedantismo: ele pega os erros clássicos que causam incidentes — FQCN faltando, shell onde command bastaria, variáveis não usadas, include no lugar de include_tasks. Em um acervo com 100+ arquivos, o lint é o guardião da consistência de estilo entre autores.

2.2 Camada 1 — Check mode e diff

O Ansible tem dry-run nativo, e ele deve ser parte do ritual:

# Simula tudo, mostra o que mudaria
ansible-playbook playbook_pos-update_restaura_confd.yml \
  --check --diff \
  -e "servidores=lab-web01"

Para o playbook de restauração do conf.d — que para o Apache, apaga um diretório e restaura um backup — o --check --diff mostra exatamente quais arquivos seriam trocados antes de qualquer dano. Um detalhe importante: módulos command/shell não são idempotentes e o check mode não os executa; por isso, a regra do time é que operações destrutivas com command ganhem um stat/find prévio com when (como o playbook de ssl.conf faz: só renomeia "se o arquivo existe").

2.3 Camada 2 — Alvos de menor risco (lab → homol → prod)

A nomenclatura da infraestrutura inteira codifica o risco:

  • lab-* → ambiente de teste destrutivo;
  • hx* (homologação) → espelho da produção com dados sintéticos;
  • px* (produção) → onde o erro doi.

E os playbooks são criados e validados nessa ordem:

# 1º: um único host de lab
ansible-playbook playbook_reboot.yml -e 'servidores=lab-web01'

# 2º: grupo de homologação
ansible-playbook playbook_reboot.yml -e 'servidores=hx*'

# 3º: produção, sempre com limit/serial
ansible-playbook playbook_reboot.yml -e 'servidores=px*'

Um truque poderoso dessa fase: os playbooks de informação (version_kernel com uptime, win_uptime com LastBootUpTime) servem como testes de sanidade. Você roda primeiro o playbook de "verificar versão do kernel" contra o alvo; se a conectividade, autenticação e facts funcionam, a mesma rota está boa para o reboot na janela.

2.4 Camada 3 — Guard-rails na execução

Além dos testes "clássicos", a base usa validações pós-ação embutidas nos playbooks. Exemplo do start do JS7 em produção:

- name: Validar processos JS7 (PROD)
  ansible.builtin.shell: "jps -l | egrep 'XmlConfiguration|ControllerMain' | awk '{print $1}'"
  register: js7_pids_valida
  changed_when: false
  failed_when: acao == 'START' and js7_pids_valida.stdout_lines | length == 0

- name: Exibir resultado
  ansible.builtin.debug:
    msg: >-
      {% if acao == 'START' and js7_pids_valida.stdout_lines | length > 0 %}
        JS7 iniciado com sucesso — PIDs: {{ js7_pids_valida.stdout_lines | join(', ') }}
      {% elif acao == 'STOP' %}
        JS7 interrompido com sucesso.
      {% else %}
        Falha na ação {{ acao }} — verifique manualmente os processos.
      {% endif %}

Isso é o que chamo de teste de asserção no playbook: o "start" só é considerado sucesso se os processos Java estão de fato de pé. A saída legível do debug vira o relatório da janela, visível no log do job da plataforma.

Resumindo a receita de testes:

CamadaFerramentaO que valida
0. Lintansible-lint, yamllint, --syntax-checkEstrutura, estilo, erros de digitação
1. Dry-run--check --diffEstado declarativo que mudaria
2. Alvos graduaislab → homol → prodConectividade, auth, efeito real
3. Asserçõesregister + failed_when + debugResultado de negócio (serviço de pé)

3. Implantação em produção

O salto de qualidade não está no playbook — está em como ele é exposto à operação. Nesse acervo, todos os playbooks rodam num Automation Platform (AAP/AWX), e a arquitetura de exposição tem cinco peças.

3.1 Surveys: a interface do operador

Cada playbook vira um Job Template com survey — um formulário que o operador preenche sem ver uma linha de YAML:

  • acao: START | STOP → mesmo template liga e desliga o ambiente;
  • servidores → alvo dinâmico (all, grupo, host);
  • ambiente: PROD | NAO_PROD | LAB → ramifica a lógica;
  • prioridade: SIM | NÃO → escolhe a fila de patch/reboot;
  • name_maintenance, time, collect → parâmetros da janela de manutenção.

O playbook só cuida de tratar as entradas com valores padrão seguros:

vars:
  servidores: "{{ servidores | default('all') }}"
  prioridade: "{{ prioridade | default('NÃO') }}"   # falha segura: NADA roda

A variável acao fica de fora dos vars — se ela não vier do survey, o job roda em modo no-op e falha visivelmente ao tentar qualquer avaliação. É um padrão intencional de "entrada obrigatória".

3.2 Workflows: encadeando playbooks como pipeline

O recurso set_stats é a cola entre templates de um workflow:

# playbook_seleciona_hosts.yml — play 2
- name: Agregar hosts do ambiente
  tasks:
    - name: Criar lista de hosts no ambiente
      set_fact:
        ambiente_hosts: "{{ groups['ambiente'] }}"
      run_once: true

    - name: Registrar hosts para workflow
      ansible.builtin.set_stats:
        data:
          var_hosts: "{{ ambiente_hosts }}"
      run_once: true

O primeiro playbook descobre os hosts-alvo (via facts locais), registra a lista em set_stats, e o workflow alimenta o template seguinte (patch, reboot) com extra_vars: var_hosts. É o padrão descobrir → decidir → executar, com auditoria em cada elo.

3.3 RBAC, credenciais e no_log

Credenciais (Jira, Zabbix API, tokens AAP, Nexus) moram no credential store da plataforma, nunca no playbook:

- name: Criando manutenção por Host
  community.zabbix.zabbix_maintenance:
    name: "{{ name_maintenance }}"
    ...
  # zuser/zpass vêm das credenciais do job, injetadas pela plataforma

E toda tarefa que registra dados sensíveis recebe no_log: true:

- name: Gera lista de versões disponíveis
  no_log: true
  loop: "{{ webpage.content | split('\n') }}"
  when: item is regex(regex_pega_versao)

O RBAC da plataforma garante que quem roda o template não precise de credencial nem acesso direto ao servidor — o operador tem permissão sobre o botão, não sobre a máquina. Isso muda a política de segurança: least privilege na interface de execução, não no login dos servidores.

3.4 async/poll e timeouts

Tarefas longas (stop de Tomcat que demora 10 minutos, yum update em uma fila inteira) usam execução assíncrona para não estourar o timeout do job:

ansible.builtin.command:
  cmd: ./stop-confluence.sh
async: 600        # até 10 min
poll: 5           # checa a cada 5s

O acervo mostra até dois padrões de autotuning da própria plataforma: um playbook que consulta o valor atual de ANSIBLE_FACT_CACHE_TIMEOUT na API do AAP, sobe para 300 segundos, dispara o job de coleta de facts e devolve o valor original — tudo com ansible.controller.settings + uri + set_fact para preservar o estado anterior.

3.5 Coordenando com o monitoramento: modo manutenção no Zabbix

Patch e reboot geram alertas falsos se o monitoramento não for avisado. O playbook de janela de manutenção usa a coleção oficial da Zabbix através do protocolo httpapi, tratando o próprio servidor Zabbix como "host" gerenciado:

- name: Colocar em manutenção os Hosts e Host Groups
  hosts: all
  vars:
    ansible_network_os: community.zabbix.zabbix
    ansible_connection: httpapi
    ansible_httpapi_use_ssl: true
  tasks:
    - name: Criando manutenção por Host
      community.zabbix.zabbix_maintenance:
        name: "{{ name_maintenance }}"
        host_name: "{{ vhost_name | trim }}"
        minutes: "{{ time }}"
        state: present
        collect_data: "{{ collect }}"
      when: manut == 'Hosts'

O fluxo de produção real (que veremos como estudo de caso) vira: 1) manutenção no Zabbix → 2) stop dos serviços → 3) patch → 4) start → 5) validação → 6) fim da manutenção — tudo encadeado como workflow na plataforma.


4. Casos de uso interessantes

Agora os casos que separam "brinquedo" de "ferramenta de operação". Todos saíram diretamente do acervo.

4.1 Pipeline de patch management multiescalonado (Linux)

Um playbook só, três levas de execução:

- name: EXECUTANDO YUM UPDATE COM EXCEÇÕES OPENLDAP
  vars:
    prioridade: "{{ prioridade | default('NÃO') }}"
  tasks:
    - name: Servidores prioritários
      block:
        - name: Atualizar pacotes (padrão)
          ansible.builtin.yum:
            name: "*"
            state: latest
          when: "'xat' not in inventory_hostname"

        - name: Atualizar pacotes (excluindo OpenLDAP)
          ansible.builtin.yum:
            name: "*"
            exclude: openldap*
          when: "'xat' in inventory_hostname"

        - name: Limpar cache do yum
          ansible.builtin.shell: yum clean all
      when: prioridade == 'SIM' and
            inventory_hostname in prioritarios_homol + prioritarios_producao

    - name: Servidores não prioritários
      block: ...
      when: prioridade == 'NÃO' and
            inventory_hostname not in prioritarios_homol + prioritarios_producao

A operação roda a mesma janela várias vezes: prioridade=SIM para o core, prioridade=NÃO para o resto. Exceções de pacote (OpenLDAP não pode ser tocado) e limpeza de repositórios fora do padrão (com renomeação seguida de timestamp em vez de exclusão, para reverter se necessário) completam o ciclo, tudo sempre precedido do backup e sucedido da restauração do conf.d.

4.2 Automação de atualizações Windows orientada a WSUS

O lado Windows trabalha em conjunto. Aprovação de updates no WSUS via PowerShell:

- name: Aprovar Atualizações no WSUS
  tasks:
    - name: Consultando atualizações disponíveis
      ansible.windows.win_shell:
        Get-WsusUpdate -Classification "{{ update_classification }}" \
          -Approval AnyExceptDeclined -Status FailedOrNeeded
      register: wsus_output

    - name: Aprovando atualizações WSUS
      ansible.windows.win_shell:
        Get-WsusUpdate ... | Approve-WsusUpdate -Action Install \
          -TargetGroupName "{{ item }}"
      loop: "{{ target_group_name }}"

E a aplicação nos servidores é uma linha declarativa — com debug de aviso antes:

- name: Aplicando atualizações disponíveis
  ansible.windows.win_updates:
    category_names: '*'
    reboot: yes

win_updates com reboot: yes é o exemplo perfeito de módulo que gerencia o ciclo de vida: instala, reinicia, espera o host voltar e continua da mesma marca. Depois, rotinas de "acordar" o cliente Windows Update (wuauserv garantido em auto + rodando) e wuauclt /detectnow / /reportnow completam o ciclo.

4.3 Start/stop orquestrado com limpeza de processos zumbis

O padrão mais recorrente do acervo. Serviços Java legados raramente morrem com um script de stop — sobra processo zumbi. O playbook trata o ciclo completo:

# 1. Stop "civilizado"
- name: Stop SimSID
  ansible.builtin.command:
    cmd: ./simsid-stop.sh
    chdir: /opt/aplicacoes/simsid/scripts
  become_user: usrsimsid

# 2. Detectar sobreviventes pelo classpath do Java
- name: Verificar PIDs SimSID
  ansible.builtin.shell: "jps -l | egrep 'simsid-web-.*\\.jar' | awk '{print $1}'"
  register: pids_simsid
  changed_when: false

# 3. Matar o que sobrou, se houver
- name: Kill PIDs SimSID
  ansible.builtin.shell: "kill -9 {{ item }}"
  loop: "{{ pids_simsid.stdout_lines }}"
  when: pids_simsid.stdout_lines | length > 0

Três decisões de engenharia aqui merecem destaque:

  1. jps -l em vez de pgrep -f: para processos Java, casar pelo classpath da aplicação é muito mais preciso que casar por nome de JAR genérico (evita matar o JRE errado).
  2. Loop sobre a lista de PIDs com when de lista vazia: cada PID é tratado individualmente; lista vazia = tarefa pulada sem erro.
  3. changed_when/failed_when calibrados: a verificação é sempre "sem mudança", e um PID que desaparece entre o register e o kill não derruba o job.

4.4 Orquestração de dependências: o caso Protheus

Start e stop de um ERP são grafos de dependência, e os dois playbooks caminham direções opostas:

# START: license server → dbaccess → app servers (ordem de dependência)
- name: Start License Server Não Produção
  hosts: app-lic01.exemplo.org.br      # licenças primeiro

- name: Start DBAccess Não Produção
  hosts: app-db01.exemplo.org.br       # banco de aplicações

- name: Start AppServers Não Produção
  hosts: app-as01..., app-as02...      # apps por último
  tasks:
    - name: Aguardar 10 segundos
      ansible.builtin.pause:
        seconds: 10

O pause de 10s entre camadas é o intervalo de estabilização. No stop, a ordem inverte (apps → dbaccess → license). Note o truque de um play do playbook que cobre dois hosts com when por inventory_hostname:

- name: Iniciar serviços no app-as01
  systemd:
    name: "{{ item }}"
    state: started
  loop: [appsrv_dev_rh, appsrv_dev_fin, appsrv_dev_lab]
  when: inventory_hostname == "app-as01..." and ambiente == 'NAO_PROD'

E o async: 600 / poll: 5 no license server — serviço que demora a subir — demonstra que mesmo o systemd merece tratamento assíncrono quando o tempo de inicialização é imprevisível.

4.5 Metadados por host: os "local facts" como coluna de decisão

Como o inventário dinâmico não sabia que um host era "prioritário", a solução foi gravar metadados no próprio host como facts locais, versionados via playbook:

# playbook_setfacts.yml — instala o "cartão de identidade" do host
- name: Configurar sessão [info]
  ansible.builtin.lineinfile:
    path: /etc/ansible/facts.d/arquivo.fact
    line: '[info]'
    create: yes

- name: Configurando tipo no arquivo de fato
  ansible.builtin.lineinfile:
    path: /etc/ansible/facts.d/arquivo.fact
    line: 'tipo={{ var_tipo }}'
  vars:
    var_tipo: "{{ tipo | default('prod') }}"

- name: Configurando prioridade
  ansible.builtin.lineinfile:
    path: /etc/ansible/facts.d/arquivo.fact
    line: 'prioridade={{ var_prioridade }}'

E o playbook "seleciona hosts" usa esses fatos para montar grupos dinâmicos em runtime (group_by) e exportar para o workflow (set_stats). Resultado: um único job template "seleciona hosts do ambiente X" alimenta qualquer automação subsequente, sem inventário estático dessincronizado.

4.6 Cadência de versões: o pipeline do agente Zabbix (self-updating fleet)

O caso mais criativo do acervo. Um playbook que roda fora dos servidores gerenciados e orquestra um ciclo de release contínuo:

  1. Lê a configuração corrente do repositório Nexus (um YAML com versao_maior, versao_menor, atualizar: true/false);
  2. Rasp a página de releases oficial do Zabbix aplicando um regex por versão;
  3. Ordena semanticamente com community.general.version_sort e pega a última versão;
  4. Compara ((zabbix_latest_minor | int) > (versao_menor | int)) e decide se há update;
  5. Baixa o MSI mais recente, publica no Nexus (tasks reutilizadas via include_tasks);
  6. Reescreve o YAML de controle com ansible.builtin.replace (flip do atualizar e bump da versao_menor);
  7. Um playbook irmão roda na frota Windows, baixa o MSI do Nexus, valida com win_stat e instala com win_package — e um terceiro só desliga a flag para a frota de VMs VMware.

Um exemplo da elegância do passo 6 — a configuração de controle é o próprio YAML, atualizado declarativamente:

- name: Marcar atualizar como true se versão for maior
  ansible.builtin.replace:
    path: "{{ versao_agente_path }}"
    regexp: '^atualizar:.*$'
    replace: "atualizar: true"
  when: (zabbix_latest_minor | int) > (versao_menor | int)

Isso é um sistema de eventos de patch: o playbook descobridor emite a "flag" (atualizar: true) e a frota inteira reage na próxima execução agendada. Nenhum humano tocou em servidor; só em um arquivo YAML que todos leem.

4.7 Integração com ITSM: playbooks que conversam com o Jira

Um playbook dedicado trata o service desk como API:

- name: Recupera dados de uma demanda
  community.general.jira:
    uri: "{{ server }}"
    username: "{{ user }}"
    password: "{{ pass }}"
    operation: fetch
    issue: "{{ demanda }}"
  register: issue
  when: acao == 'Existente'

- name: Dados da demanda
  debug:
    msg:
      - "Tipo da demanda: {{ issue.meta.fields.issuetype.name }}"
      - "Situação:        {{ issue.meta.fields.status.name }}"

Com register: issue, os metadados do chamado (issuetype, reporter, parent, status, worklog) ficam disponíveis para lógica subsequente — o playbook pode criar a demanda filha, comentar com visibilidade restrita a um role e marcar comentários como internos, todos os fluxos de "automação que atualiza o chamado" de uma janela de manutenção.

4.8 O ciclo completo em produção

Juntando tudo, o fluxo de uma janela de manutenção real fica:

┌─ Workflow AAP ──────────────────────────────────────────────────┐
│                                                                 │
│  1. Seção hosts via survey                    (job template)    │
│  2. set_stats var_hosts                     (descobre alvos)    │
│  3. Zabbix maintenance (host/group)         (silencia alertas)  │
│  4. WSUS approve                            (Windows)           │
│  5. yum update / win_updates                (patch, com reboot) │
│  6. Pre-update backup → patch → pos-update restore             │
│  7. Start/stop com validação de PIDs        (serviços)          │
│  8. win_atualiza_zabbix_agent / pos-update zabbix agent         │
│  9. Jira: comentário com resultado          (auditoria)         │
│ 10. Remover manutenção                      (reativar alertas)  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Cada passo é independente, auditável e re-executável. Falhou o passo 6? O workflow reinicia dali, porque os playbooks anteriores são idempotentes ou possuem backup anterior íntegro.


5. Lições e checklist referência

O acervo é revelador das boas práticas que sobreviveram ao teste de produção — e das armadilhas que eles corrigem entre versões (o _v2 dos playbooks é a história viva do aprendizado).

Checklist do playbook pronto para produção

  • hosts: "{{ variavel }}" com default() seguro no nível vars
  • gather_facts: false + setup com gather_subset quando facts forem necessários
  • Valores operacionais em vars_files versionados
  • Todo command/shell com changed_when, failed_when (ou justificativa p/ ignore_errors)
  • Operações destrutivas precedidas de validação (stat/find) e backup com sufixo reversível
  • Credenciais na plataforma (nunca no YAML) + no_log: true onde logaria segredo
  • async/poll para tarefas > 2 min
  • Asserções pós-ação para serviços críticos ("subiu mesmo?") com failed_when
  • Comentário de cabeçalho explicando o que o playbook faz e por quem é executado
  • Passou por ansible-lint, yamllint, --syntax-check, --check --diff

Lições que valem um post separado

  1. O padrão "playbook genérico + survey" escala melhor que "playbook por caso" — e a reação natural (criar um playbook novo a cada necessidade) é a dívida técnica mais comum de times iniciantes em automação.
  2. Fatos locais são o plano B do inventário dinâmico — quando o CMDB/inventário não tem o dado, o host pode ser a fonte da verdade dele mesmo.
  3. ignore_errors é decisão, não descuido — cada uso precisa de um comentário dizendo por quê (janela best effort? erro tolerado?).
  4. A configuração da plataforma é administrável por playbook — o AAP é um produto API-first; ajustar o timeout do fact cache, disparar templates e ler seus resultados são tarefas automatizáveis.
  5. A ordem de stop é espelho da ordem de start — documente as dependências no nome dos plays ou em comentários, porque a proxima pessoa que editar o playbook às 3h da manhã não vai ter tempo de lembrar disso.
  6. Renomear com timestamp > deletar — .disable_2025-11-14_03-15-44 é sempre reversível; rm não é.

Conclusão

Uma base de playbooks de produção conta uma história: começou com yum update, ganhou exceções, ganhou listas versionadas, ganhou survey, ganhou workflow, ganhou validação pós-ação, ganhou integração com o monitoramento, o ITSM e a plataforma de automação. O Ansible é o denominador comum, mas a engenharia está nos padrões ao redor — alvo dinâmico, lógica por blocos, teste em camadas e exposição segura via plataforma.

O convite que fica: olhe seus playbooks com as lentes deste artigo. Pergunte-se a cada um: "Se eu rodar isso errado, quanto custa? E o que impediria?". Se a resposta for "nada" — agora você sabe os padrões para consertar.


Gostou do tema? Nos próximos artigos: testes com molecule para roles, Ansible Event-Driven (EDA) reagindo a alertas do Zabbix em tempo real e CI/CD de playbooks com validação em containers descartáveis.