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:
- A lógica básica de desenvolvimento de playbooks (as decisões que se repetem em 90% dos casos);
- Estruturas de testes que dão coragem para executar na produção;
- O caminho de implantação em produção via plataforma de automação (AAP/AWX) com surveys, workflows e RBAC;
- 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: allA 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/ambienteIsso 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.ymlOs 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_producaoRepare 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 baileE 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.ymlO 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:
| Camada | Ferramenta | O que valida |
|---|---|---|
| 0. Lint | ansible-lint, yamllint, --syntax-check | Estrutura, estilo, erros de digitação |
| 1. Dry-run | --check --diff | Estado declarativo que mudaria |
| 2. Alvos graduais | lab → homol → prod | Conectividade, auth, efeito real |
| 3. Asserções | register + failed_when + debug | Resultado 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 rodaA 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: trueO 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 plataformaE 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 5sO 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_producaoA 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: yeswin_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 > 0Três decisões de engenharia aqui merecem destaque:
jps -lem vez depgrep -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).- Loop sobre a lista de PIDs com
whende lista vazia: cada PID é tratado individualmente; lista vazia = tarefa pulada sem erro. changed_when/failed_whencalibrados: a verificação é sempre "sem mudança", e um PID que desaparece entre oregistere okillnã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: 10O 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:
- Lê a configuração corrente do repositório Nexus (um YAML com
versao_maior,versao_menor,atualizar: true/false); - Rasp a página de releases oficial do Zabbix aplicando um regex por versão;
- Ordena semanticamente com
community.general.version_sorte pega a última versão; - Compara (
(zabbix_latest_minor | int) > (versao_menor | int)) e decide se há update; - Baixa o MSI mais recente, publica no Nexus (tasks reutilizadas via
include_tasks); - Reescreve o YAML de controle com
ansible.builtin.replace(flip doatualizare bump daversao_menor); - Um playbook irmão roda na frota Windows, baixa o MSI do Nexus, valida com
win_state instala comwin_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 }}"comdefault()seguro no nívelvars -
gather_facts: false+setupcomgather_subsetquando facts forem necessários - Valores operacionais em
vars_filesversionados - Todo
command/shellcomchanged_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: trueonde logaria segredo -
async/pollpara 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
- 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.
- 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.
ignore_errorsé decisão, não descuido — cada uso precisa de um comentário dizendo por quê (janela best effort? erro tolerado?).- 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.
- 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.
- Renomear com timestamp > deletar —
.disable_2025-11-14_03-15-44é sempre reversível;rmnã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.