Cultura DevOps: Maturidade, Postmortems e Melhoria Contínua

[315] Cultura DevOps: Maturidade, Postmortems e Melhoria Contínua

O que separa uma organização DevOps de alto desempenho de uma medíocre depois que as ferramentas estão todas instaladas: as quatro métricas DORA e como coletá-las, postmortems sem culpa que geram aprendizado real, os rituais de melhoria contínua e o modelo de Westrum de cultura organizacional.
DevOps

29 min de leitura

É possível instalar todas as ferramentas descritas nos artigos anteriores (Performance e FinOps: Otimizando Custo e Velocidade na Cloud, Resiliência e Chaos Engineering, Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento) — Kubernetes, ArgoCD, Prometheus, Falco, Backstage — e ainda assim ter uma organização que entrega software lentamente, sofre incidentes frequentes e não aprende com seus erros. As ferramentas são necessárias mas não suficientes. O que determina se uma organização DevOps é de alto desempenho ou medíocre é, em última análise, cultural: como as pessoas colaboram, como respondem a falhas, como tomam decisões e como aprendem.

O relatório State of DevOps, produzido anualmente pelo DORA — DevOps Research and Assessment — é o estudo longitudinal mais abrangente sobre performance em entrega de software. Ao longo de mais de uma década de pesquisa com dezenas de milhares de organizações, o DORA identificou que os maiores preditores de performance organizacional em entrega de software não são as ferramentas usadas, mas práticas culturais: psicologia de segurança para relatar problemas sem medo de punição, trabalho em pequenos lotes, colaboração entre times e foco em aprendizado contínuo.

Este artigo explora como medir a maturidade de uma organização DevOps, como conduzir postmortems que geram aprendizado real em vez de relatórios que são arquivados, e como construir os rituais de melhoria contínua que sustentam a evolução ao longo do tempo.

Medindo Maturidade DevOps com as Métricas DORA

As quatro métricas DORA — introduzidas no artigo Publicando Imagens no Docker Hub e GitHub Container Registry em seu contexto de pipeline — são, em sua forma completa, métricas de resultado organizacional que medem a capacidade de entrega de valor com qualidade:

Deployment Frequency — com que frequência a organização faz deploy em produção. Times de elite fazem múltiplos deploys por dia. Times de baixo desempenho fazem deploys mensais ou menos frequentes.

Lead Time for Changes — tempo desde o commit até o código estar em produção. Times de elite medem em minutos a horas. Times de baixo desempenho medem em semanas a meses.

Change Failure Rate — percentual de deploys que resultam em degradação de serviço ou incidente. Times de elite mantêm abaixo de 5%. Times de baixo desempenho chegam a 46-60%.

Time to Restore Service — tempo para restaurar o serviço após um incidente. Times de elite restauram em menos de uma hora. Times de baixo desempenho levam de uma semana a um mês.

Coletando as Métricas DORA Automaticamente

// scripts/coletar-metricas-dora.js
// Coleta as quatro métricas DORA via GitHub API e dados de incidente
// Executa diariamente via pipeline e publica no Prometheus Pushgateway

const { Octokit } = require('@octokit/rest');
const { Pushgateway, Gauge } = require('prom-client');

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const pushgateway = new Pushgateway(process.env.PROMETHEUS_PUSHGATEWAY);

const ORG = process.env.GITHUB_ORG;
const REPO = process.env.GITHUB_REPO;
const JANELA_DIAS = 30;

// ── Deployment Frequency ─────────────────────────────────────────────

async function calcularDeployFrequency() {
  const fim = new Date();
  const inicio = new Date(fim - JANELA_DIAS * 24 * 60 * 60 * 1000);

  // Busca todos os deployments no período
  const { data: deployments } = await octokit.repos.listDeployments({
    owner: ORG,
    repo: REPO,
    environment: 'production',
    per_page: 100,
  });

  const deploymentsNoPeriodo = deployments.filter(d => {
    const criado = new Date(d.created_at);
    return criado >= inicio && criado <= fim;
  });

  // Conta apenas deployments bem-sucedidos
  const deploymentsSuccessful = await Promise.all(
    deploymentsNoPeriodo.map(async d => {
      const { data: statuses } = await octokit.repos.listDeploymentStatuses({
        owner: ORG,
        repo: REPO,
        deployment_id: d.id,
        per_page: 1,
      });
      return statuses[0]?.state === 'success';
    })
  );

  const totalSuccessful = deploymentsSuccessful.filter(Boolean).length;
  const deploysPerDia = totalSuccessful / JANELA_DIAS;

  return {
    total: totalSuccessful,
    porDia: deploysPerDia,
    classificacao: classificarDeployFrequency(deploysPerDia),
  };
}

function classificarDeployFrequency(deploysPerDia) {
  if (deploysPerDia >= 1)   return 'elite';    // >= 1 deploy/dia
  if (deploysPerDia >= 0.14) return 'alto';    // >= 1/semana
  if (deploysPerDia >= 0.03) return 'medio';   // >= 1/mês
  return 'baixo';
}

// ── Lead Time for Changes ─────────────────────────────────────────────

async function calcularLeadTime() {
  const fim = new Date();
  const inicio = new Date(fim - JANELA_DIAS * 24 * 60 * 60 * 1000);

  // Busca PRs mergeados no período
  const { data: pulls } = await octokit.pulls.list({
    owner: ORG,
    repo: REPO,
    state: 'closed',
    base: 'main',
    per_page: 100,
    sort: 'updated',
    direction: 'desc',
  });

  const prsMergeados = pulls.filter(pr => {
    if (!pr.merged_at) return false;
    const mergedAt = new Date(pr.merged_at);
    return mergedAt >= inicio && mergedAt <= fim;
  });

  if (prsMergeados.length === 0) {
    return { mediaMinutos: null, classificacao: 'sem-dados' };
  }

  // Calcula o tempo desde o primeiro commit até o merge
  const leadTimes = await Promise.all(
    prsMergeados.map(async pr => {
      const { data: commits } = await octokit.pulls.listCommits({
        owner: ORG,
        repo: REPO,
        pull_number: pr.number,
        per_page: 1,
      });

      // Usa a data do primeiro commit como início do lead time
      const primeiroCommit = commits[commits.length - 1];
      if (!primeiroCommit) return null;

      const inicio = new Date(primeiroCommit.commit.author.date);
      const fim = new Date(pr.merged_at);
      return (fim - inicio) / (1000 * 60); // Converte para minutos
    })
  );

  const leadTimesValidos = leadTimes.filter(Boolean);
  const media = leadTimesValidos.reduce((a, b) => a + b, 0) / leadTimesValidos.length;

  return {
    mediaMinutos: Math.round(media),
    mediaHoras: Math.round(media / 60),
    classificacao: classificarLeadTime(media),
  };
}

function classificarLeadTime(minutos) {
  const horas = minutos / 60;
  if (horas <= 24)      return 'elite';   // < 1 dia
  if (horas <= 168)     return 'alto';    // < 1 semana
  if (horas <= 720)     return 'medio';   // < 1 mês
  return 'baixo';
}

// ── Change Failure Rate ───────────────────────────────────────────────

async function calcularChangeFailureRate() {
  const fim = new Date();
  const inicio = new Date(fim - JANELA_DIAS * 24 * 60 * 60 * 1000);

  // Busca deployments totais
  const { data: deployments } = await octokit.repos.listDeployments({
    owner: ORG,
    repo: REPO,
    environment: 'production',
    per_page: 100,
  });

  const deploymentsNoPeriodo = deployments.filter(d =>
    new Date(d.created_at) >= inicio
  );

  // Busca incidentes do período via PagerDuty API
  const incidentesResponse = await fetch(
    `https://api.pagerduty.com/incidents?` +
    `since=${inicio.toISOString()}&until=${fim.toISOString()}&` +
    `service_ids[]=${process.env.PAGERDUTY_SERVICE_ID}`,
    {
      headers: {
        Authorization: `Token token=${process.env.PAGERDUTY_TOKEN}`,
        Accept: 'application/vnd.pagerduty+json;version=2',
      },
    }
  );

  const { incidents } = await incidentesResponse.json();

  // Incidentes causados por deploy (tem tag "deploy-related")
  const incidentesDeployRelated = incidents.filter(i =>
    i.tags?.some(t => t.label === 'deploy-related')
  );

  const cfr = deploymentsNoPeriodo.length > 0
    ? (incidentesDeployRelated.length / deploymentsNoPeriodo.length) * 100
    : 0;

  return {
    totalDeploys: deploymentsNoPeriodo.length,
    totalIncidentes: incidentesDeployRelated.length,
    percentual: Math.round(cfr * 10) / 10,
    classificacao: classificarCFR(cfr),
  };
}

function classificarCFR(percentual) {
  if (percentual <= 5)  return 'elite';
  if (percentual <= 15) return 'alto';
  if (percentual <= 45) return 'medio';
  return 'baixo';
}

// ── Publica métricas no Prometheus ───────────────────────────────────

async function publicarMetricas() {
  const [deployFreq, leadTime, cfr] = await Promise.all([
    calcularDeployFrequency(),
    calcularLeadTime(),
    calcularChangeFailureRate(),
  ]);

  const metricaDeployFreq = new Gauge({
    name: 'dora_deployment_frequency_daily',
    help: 'Número médio de deploys por dia nos últimos 30 dias',
    labelNames: ['repositorio', 'classificacao'],
  });

  const metricaLeadTime = new Gauge({
    name: 'dora_lead_time_minutes',
    help: 'Lead time médio em minutos nos últimos 30 dias',
    labelNames: ['repositorio', 'classificacao'],
  });

  const metricaCFR = new Gauge({
    name: 'dora_change_failure_rate_percent',
    help: 'Taxa de falha de mudanças em percentual nos últimos 30 dias',
    labelNames: ['repositorio', 'classificacao'],
  });

  metricaDeployFreq.set(
    { repositorio: REPO, classificacao: deployFreq.classificacao },
    deployFreq.porDia
  );

  if (leadTime.mediaMinutos) {
    metricaLeadTime.set(
      { repositorio: REPO, classificacao: leadTime.classificacao },
      leadTime.mediaMinutos
    );
  }

  metricaCFR.set(
    { repositorio: REPO, classificacao: cfr.classificacao },
    cfr.percentual
  );

  await pushgateway.pushAdd({
    jobName: 'dora-metrics',
    groupings: { repositorio: REPO },
  });

  console.log(JSON.stringify({
    level: 'info',
    msg: 'Métricas DORA publicadas',
    deployFrequency: deployFreq,
    leadTime,
    changeFailureRate: cfr,
  }, null, 2));
}

publicarMetricas().catch(console.error);

Postmortems Sem Culpa: Aprendendo com Incidentes

O postmortem — também chamado de incident review ou after-action review — é o processo estruturado de analisar um incidente após sua resolução para extrair aprendizados e definir ações que evitem recorrência. A qualidade cultural de uma organização se revela mais claramente em como ela conduz seus postmortems do que em quase qualquer outra prática.

A Cultura de Não-Culpa

O princípio da culpa zero — blameless postmortem — parte de uma premissa fundamentada em décadas de pesquisa em segurança industrial: em sistemas complexos, incidentes são quase sempre o resultado de múltiplos fatores que se combinaram de maneira imprevista, não da incompetência ou negligência de um indivíduo.

Quando uma organização trata incidentes como oportunidades para encontrar e punir culpados, produz três efeitos destrutivos: as pessoas deixam de reportar problemas por medo de punição; os postmortems se tornam exercícios defensivos em vez de investigativos; e as causas sistêmicas dos incidentes permanecem intocadas enquanto o indivíduo "culpado" é removido ou punido.

A cultura de não-culpa não significa ausência de responsabilidade — significa que a responsabilidade é coletiva e sistêmica. O engenheiro que fez o deploy que causou o incidente não é culpado; o sistema que permitiu aquele deploy sem os controles adequados é o problema a ser corrigido.

Estrutura de um Postmortem Eficaz

# Postmortem: Indisponibilidade do Serviço de Checkout
**Data do incidente:** 2025-03-10
**Duração:** 47 minutos (14h23 — 15h10 UTC)
**Severidade:** SEV-1
**Facilitador:** Ana Costa
**Participantes:** Pedro Silva, Luiza Mendes, Carlos Santos, Maria Lima

---

## Sumário Executivo

Indisponibilidade completa do serviço de checkout durante 47 minutos,
afetando aproximadamente 2.300 usuários. Causa raiz: migration de banco
de dados que adicionou índice em tabela de pedidos (23M linhas) sem
usar criação concorrente, bloqueando todas as escritas durante a
construção do índice. Estimativa de impacto: R$ 87.000 em pedidos não
concluídos no período.

---

## Linha do Tempo

| Hora (UTC) | Evento                                                                 |
|------------|------------------------------------------------------------------------|
| 14:18      | Deploy v2.4.1 iniciado via pipeline de CI/CD                          |
| 14:23      | Migration executada — início da construção do índice em produção      |
| 14:24      | Primeiros alertas de timeout no Grafana (latência p99 > 5s)           |
| 14:26      | PagerDuty aciona plantão — Pedro Silva                                 |
| 14:31      | Pedro identifica a migration como causa provável via Performance Insights |
| 14:35      | Decisão de rollback discutida — migration não é facilmente reversível  |
| 14:40      | Opção escolhida: cancelar a construção do índice via pg_cancel_backend |
| 14:52      | Índice cancelado — serviço começa a se recuperar                      |
| 15:10      | Todos os pods saudáveis, métricas normalizadas                         |
| 15:15      | Incidente declarado resolvido                                          |
| 15:45      | Comunicação enviada para clientes afetados                             |

---

## Causa Raiz

A migration `20250310_add_index_pedidos_usuario_id.sql` criou um índice
usando o comando padrão `CREATE INDEX`, que adquire um `ShareLock`
na tabela durante toda a construção. Para tabelas de 23M linhas, a
construção levou aproximadamente 29 minutos — bloqueando todas as
operações de escrita durante esse período.

O comando correto para tabelas em produção é
`CREATE INDEX CONCURRENTLY`, que constrói o índice em segundo plano
sem bloquear escritas, mas requer atenção especial (não pode ser
executado dentro de uma transaction block).

---

## Fatores Contribuintes

**Fatores técnicos:**
- Não havia validação automatizada de migrations contra tabelas grandes
  antes do deploy
- O ambiente de staging tem apenas ~50K linhas na tabela de pedidos,
  tornando o impacto invisível em testes
- Não havia alerta configurado para `lock_waits` prolongados no RDS

**Fatores de processo:**
- A revisão do PR não incluiu avaliação do impacto da migration em
  produção (revisores focaram no código da aplicação)
- Não havia um checklist de pre-deploy para migrations em tabelas grandes
- O runbook de rollback de migrations não estava atualizado

**Fatores de conhecimento:**
- A diferença entre CREATE INDEX e CREATE INDEX CONCURRENTLY
  não estava documentada nas diretrizes de migrations
- Novos membros do time não recebem treinamento específico sobre
  operações seguras em banco de dados de produção

---

## O Que Funcionou Bem

- O alerta de latência p99 disparou 2 minutos após o início do incidente —
  tempo de detecção excelente
- Pedro identificou a causa raiz em menos de 10 minutos usando
  Performance Insights e CloudWatch Logs
- A comunicação interna via Slack foi clara e frequente durante o incidente
- O time tomou a decisão correta de cancelar o índice em vez de esperar,
  evitando uma hora adicional de indisponibilidade

---

## Ações Corretivas

| Ação | Responsável | Prazo | Issue |
|------|-------------|-------|-------|
| Adicionar lint de migrations que detecta CREATE INDEX sem CONCURRENTLY em tabelas > 1M linhas | Pedro Silva | 2025-03-17 | #1847 |
| Atualizar o ambiente de staging para ter volume de dados proporcional ao de produção (sample de 10%) | Luiza Mendes | 2025-03-24 | #1848 |
| Criar checklist de pre-deploy para migrations com verificação de tamanho de tabela | Ana Costa | 2025-03-14 | #1849 |
| Adicionar alerta de lock_wait_timeout prolongado no CloudWatch | Carlos Santos | 2025-03-17 | #1850 |
| Documentar migrations seguras em tabelas grandes no wiki de engenharia | Maria Lima | 2025-03-21 | #1851 |
| Incluir módulo de banco de dados em produção no onboarding de novos engenheiros | Ana Costa | 2025-03-28 | #1852 |

---

## Métricas do Incidente

- **MTTD** (Mean Time to Detect): 3 minutos
- **MTTI** (Mean Time to Identify): 8 minutos  
- **MTTR** (Mean Time to Restore): 47 minutos
- **Usuários afetados:** ~2.300
- **Impacto estimado:** R$ 87.000

---

## Aprendizados para o Time

Esta não é a primeira vez que uma migration de banco de dados causa
incidente — em agosto de 2024 tivemos um incidente similar por um
ALTER TABLE sem DEFAULT em tabela grande. O padrão indica uma lacuna
sistêmica em nosso processo de validação de migrations, não um
problema de competência individual.

A ação de maior impacto não é o lint (que é preventivo), mas a
paridade de dados entre staging e produção — sem ela, nenhuma
quantidade de revisão manual detectará problemas de escala.

Facilitando um Postmortem Eficaz

O facilitador do postmortem tem a responsabilidade de criar um ambiente onde as pessoas sintam segurança para compartilhar o que realmente aconteceu:

## Guia do Facilitador de Postmortem

### Antes da reunião
- [ ] Leia todos os logs e alertas do período do incidente
- [ ] Construa um rascunho da linha do tempo para validar com participantes
- [ ] Identifique as perguntas que ainda precisam de resposta
- [ ] Confirme que todos os participantes relevantes estão convidados
- [ ] Reserve 60–90 minutos (incidentes complexos podem precisar de mais)

### Durante a reunião

**Abertura (5 min)**
Reforce explicitamente que o objetivo é aprender, não atribuir culpa.
Exemplo de fala: "Estamos aqui para entender o que aconteceu e o que
podemos melhorar como sistema e como time. Não há respostas erradas,
e ninguém será julgado pelas decisões que tomou com as informações
que tinha no momento."

**Revisão da linha do tempo (15–20 min)**
Percorra a linha do tempo cronologicamente. Para cada evento, pergunte:
- O que você estava vendo nesse momento?
- Qual informação você tinha disponível?
- O que você considerou antes de tomar essa decisão?

**Análise de causa raiz (20–30 min)**
Use os "5 Porquês" iterativamente até chegar a causas sistêmicas.
Evite parar no primeiro "erro humano" — pergunte: "O que no sistema
tornou esse erro possível?"

Exemplo:
- Por que o serviço ficou indisponível? → Migration bloqueou escritas
- Por que a migration bloqueou escritas? → CREATE INDEX sem CONCURRENTLY
- Por que foi usado CREATE INDEX sem CONCURRENTLY? → Desenvolvedor
  não conhecia a diferença
- Por que o desenvolvedor não conhecia? → Não há documentação nem
  treinamento específico sobre isso
- Por que não há documentação? → Nunca foi priorizado formalmente
→ Causa sistêmica: lacuna de conhecimento não documentada

**Definição de ações (15–20 min)**
Para cada causa sistêmica, defina uma ação com responsável e prazo.
Prefira ações preventivas (detecção automática) sobre ações paliativas
(treinamento manual). Não defina mais de 5–7 ações — priorize as de
maior impacto.

**Encerramento (5 min)**
Agradeça a participação de todos. Reforce que o postmortem será
publicado internamente para que toda a organização possa aprender.

### Após a reunião
- [ ] Publique o postmortem no wiki de engenharia em até 48 horas
- [ ] Crie os issues/tickets para todas as ações definidas
- [ ] Agende revisão de 30 dias para verificar o progresso das ações
- [ ] Atualize o dashboard de incidentes com os dados do postmortem

Rituais de Melhoria Contínua

Os postmortems tratam de incidentes específicos. A melhoria contínua requer rituais que operem em cadências diferentes — diária, semanal, mensal, trimestral:

Retrospectiva Semanal do Time

## Formato de Retrospectiva (60 minutos, sexta-feira)

### Check-in (5 min)
Cada pessoa compartilha uma palavra sobre como está chegando à retro.

### Revisão da semana (10 min)
- Deploys realizados: X
- Incidentes: X (severidade Y)
- Métricas DORA da semana

### O que foi bem (15 min)
Cada pessoa nomeia uma coisa que funcionou bem.
Time vota nas 3 mais importantes para celebrar/manter.

### O que pode melhorar (15 min)
Cada pessoa nomeia uma coisa que foi difícil ou ineficiente.
Time vota nas 3 mais importantes para abordar.

### Ações (10 min)
Para cada item da lista "melhorar", define uma ação com responsável.
Máximo de 3 ações por semana — foco é melhor que quantidade.

### Revisão das ações anteriores (5 min)
Verifica o progresso das ações definidas na retro anterior.

Quarterly Business Review de Engenharia

Uma vez por trimestre, o time de engenharia revisa suas métricas de desempenho em um nível mais alto:

## Engineering QBR — Q1 2025

### Métricas DORA — Evolução Trimestral

| Métrica               | Q4 2024  | Q1 2025  | Δ       | Meta Q2   |
|-----------------------|----------|----------|---------|-----------|
| Deploy Frequency      | 2.1/dia  | 3.8/dia  | +81%    | 5.0/dia   |
| Lead Time             | 4.2h     | 2.1h     | -50%    | < 1h      |
| Change Failure Rate   | 8.3%     | 4.1%     | -51%    | < 3%      |
| MTTR                  | 42min    | 18min    | -57%    | < 15min   |

### Análise de Incidentes SEV-1 e SEV-2

Total de incidentes: 12 (vs 18 em Q4 2024 — redução de 33%)

Causas raiz recorrentes:
- Migrations de banco de dados: 4 incidentes (33%)
- Dependências externas (pagamento, email): 3 incidentes (25%)
- Configuração incorreta de feature flags: 2 incidentes (17%)
- Outros: 3 incidentes (25%)

### Toil Report

Trabalho repetitivo identificado e seu status:
- Rotação manual de credenciais: ELIMINADO (External Secrets Operator)
- Provisioning manual de ambientes: REDUZIDO 70% (Backstage templates)
- Debugging de builds falhando: EM PROGRESSO (melhores mensagens de erro)

### Investimentos para Q2

Baseado na análise acima, os três maiores investimentos para Q2:
1. Lint automático de migrations (4 incidentes prevenidos/trimestre estimado)
2. Chaos Engineering game days mensais (medir resiliência proativamente)
3. Melhorar paridade staging/produção (reduzir surpresas em deploy)

Psicologia de Segurança: A Base de Tudo

O modelo de Westrum — desenvolvido pelo sociólogo Ron Westrum para estudar culturas organizacionais em ambientes de alta complexidade como aviação e medicina — classifica organizações em três tipos:

Patológica — informação é ocultada por medo. Mensageiros são punidos. Falhas são escondidas. Novidades são disruptivas e ameaçadoras.

Burocrática — informação é compartimentada. Responsabilidades são rigidamente definidas. Processos são seguidos mesmo quando não fazem sentido.

Generativa — informação flui livremente. Responsabilidades são compartilhadas. Falhas são oportunidades de aprendizado. A missão importa mais do que o território.

O DORA demonstrou empiricamente que organizações com cultura generativa têm desempenho significativamente superior em todas as métricas de entrega de software. A cultura generativa não é um fim em si mesmo — é um pré-requisito para que todas as práticas técnicas descritos nesta série funcionem.

Um sinal concreto e mensurável de psicologia de segurança é a disposição dos engenheiros de reportar problemas antes que se tornem incidentes. Organizações que punem os portadores de más notícias rapidamente aprendem a não receber más notícias — até que se tornem incidentes de grandes proporções.

Com este artigo encerra-se o tema — Segurança e Compliance em DevOps — e com ele o conteúdo central do curso. Foram cobertos DevSecOps com integração de segurança em todo o pipeline, compliance e auditoria com geração automatizada de evidências, otimização de performance e custos com FinOps, resiliência com Chaos Engineering, Platform Engineering com Backstage e, neste artigo, a dimensão cultural que torna sustentáveis todas as práticas técnicas.

Os próximos artigos formam o projeto capstone da série — a integração de todos os conceitos em um sistema completo de produção.

Referências para Aprofundamento

Pesquisa e métricas

  • DORA State of DevOps Report — dora.dev — Relatório anual do DORA com os dados mais recentes sobre performance em entrega de software, incluindo análise das quatro métricas e dos preditores culturais de alto desempenho.
  • DORA Quick Check — dora.dev — Ferramenta online para avaliar rapidamente o desempenho da organização nas quatro métricas DORA e identificar áreas de melhoria.

Cultura e postmortems

Exercícios

Exercício 1

O calcularDeployFrequency busca os deployments com per_page: 100 e depois filtra pelos últimos 30 dias. O QBR do artigo registra 3,8 deploys/dia. Faça a conta: esse código consegue medir corretamente um time nesse patamar? E o que isso faz com o Change Failure Rate?

Ver resposta

✓ Resposta: Não consegue. A 3,8 deploys por dia, 30 dias somam 114 deployments — e a chamada traz no máximo 100. Como a API devolve os mais recentes primeiro, os 14 mais antigos da janela simplesmente não chegam, e o filtro por data opera sobre um conjunto já incompleto. A frequência calculada cai para 3,33/dia.

O que torna esse defeito notável é que ele piora conforme o time melhora. Um time medíocre com 20 deploys por mês é medido com precisão total; um time de elite com 300 deploys é medido como se tivesse feito 100. A métrica desenhada para reconhecer alto desempenho é justamente a que falha nele — e o erro sempre aponta para baixo, nunca para cima.

No Change Failure Rate a consequência é pior, porque o número de deploys é o denominador:

CFR = incidentes_deploy_related / total_deploys × 100

Os incidentes vêm do PagerDuty, sem esse teto de 100. Com 114 deploys reais, 5 incidentes e o denominador travado em 100, o CFR sai 5,0% em vez dos 4,4% reais — e 5% é exatamente a fronteira entre "elite" e "alto" na função classificarCFR. Um bug de paginação rebaixa a classificação do time.

A correção é paginar de verdade, e o Octokit já oferece o utilitário para isso:

const deployments = await octokit.paginate(
  octokit.repos.listDeployments,
  { owner: ORG, repo: REPO, environment: 'production', per_page: 100 }
);

Vale notar que esse é o mesmo defeito da coleta de evidências SOC2, no artigo sobre compliance: buscar uma página e filtrar depois. Ali ele produzia evidência incompleta para um auditor; aqui, uma métrica que orienta decisões de investimento trimestral. A forma do erro é idêntica — tratar o primeiro lote da API como se fosse o conjunto.

Exercício 2

O calcularLeadTime reduz todos os PRs a um único número com reduce((a, b) => a + b, 0) / leadTimesValidos.length. Numa semana com 19 PRs mergeados em cerca de 2 horas cada e 1 PR que ficou aberto por 3 semanas, qual o lead time reportado? Que decisão errada esse número induz?

Ver resposta

✓ Resposta: A conta dá cerca de 27 horas: (19 × 2 + 504) / 20 = 27,1. Pela função classificarLeadTime, 27 h passa de 24 h e o time é classificado como "alto" em vez de "elite" — por causa de um único PR.

A mediana desse mesmo conjunto é 2 horas. Ou seja, o fluxo real de trabalho do time é excelente e existe um outlier; a média mistura as duas informações e não representa nenhuma delas. Nenhum PR daquela semana levou 27 horas.

A decisão errada que isso induz é otimizar o processo inteiro — revisar políticas de code review, cobrar agilidade dos revisores, reorganizar cerimônias — quando o dado real diz que 19 de 20 PRs já fluem em 2 horas. O problema é aquele PR, e é uma pergunta específica: por que ele ficou 3 semanas parado? Dependência externa? Revisor ausente? Escopo grande demais? A média apagou exatamente a pergunta que valia a pena fazer.

É por isso que o DORA trabalha com mediana e percentis, nunca com média. A recomendação prática é publicar dois números lado a lado: a mediana (o caso típico) e o p90 (a cauda ruim). O primeiro diz como é o dia a dia; o segundo, quão ruim fica quando algo trava — e é a diferença entre eles que aponta onde investir.

function percentil(valores, p) {
  const ord = [...valores].sort((a, b) => a - b);
  return ord[Math.floor(ord.length * p)];
}
const mediana = percentil(leadTimesValidos, 0.5);
const p90     = percentil(leadTimesValidos, 0.9);

Há ainda um detalhe latente na mesma função. O primeiro commit é obtido com per_page: 1 e acessado por commits[commits.length - 1]. Como só vem um item, o índice resolve para 0 e o resultado está certo — por coincidência. Se alguém aumentar o per_page um dia, a expressão passa a pegar o último commit da página, e o lead time despenca artificialmente sem que nada acuse o erro.

Exercício 3

O Change Failure Rate conta apenas incidentes com a etiqueta deploy-related no PagerDuty. Um time novo adota o script e, no primeiro mês, o painel mostra CFR de 0% — classificação "elite". Por que isso deve ser tratado como suspeita, e não como conquista?

Ver resposta

✓ Resposta: Porque o CFR de 0% tem duas explicações possíveis, e a mais provável é a pior: ninguém etiquetou os incidentes. A etiqueta é aplicada manualmente por quem responde ao chamado, às vezes de madrugada, no meio da mitigação. Um time recém-chegado à prática não tem esse hábito — e a ausência do rótulo é indistinguível, para o script, da ausência de falhas.

É a assinatura de um indicador que falha em aberto: quando a coleta não acontece, o resultado não é "desconhecido", é "perfeito". O mesmo padrão do job relatorio no pipeline de segurança, que anunciava sucesso quando os jobs eram pulados. A regra vale para os dois casos: a ausência de sinal nunca deve ser lida como sinal positivo.

O agravante é o incentivo que isso cria. O CFR costuma ser exibido em QBR e comparado entre times — e a maneira mais barata de "melhorar" o número é parar de etiquetar. Ninguém precisa decidir fraudar nada: basta o hábito não pegar, e a métrica recompensa isso. Uma métrica de qualidade que pode ser zerada por omissão mede disciplina de preenchimento, não qualidade.

Duas defesas. A primeira é derivar o vínculo de dados que existem sozinhos: correlacionar o horário de abertura do incidente com a janela de deploy — um incidente aberto até 60 minutos depois de um deploy é candidato automático, restando ao humano apenas descartar os falsos positivos, que é o caminho seguro. A segunda é tornar a lacuna visível: reportar ao lado do CFR quantos incidentes ficaram sem classificação. Um mês com 0% de CFR e 7 incidentes não classificados conta uma história bem diferente de 0% com 7 incidentes analisados e descartados.

Repare que o próprio artigo dá a chave disso ao afirmar que um sinal concreto de psicologia de segurança é a disposição de reportar problemas. Um número bom demais, cedo demais, costuma medir o silêncio — não a excelência.

Exercício 4

A seção "Causa Raiz" do postmortem afirma que o CREATE INDEX "adquire um AccessShareLock na tabela durante toda a construção", bloqueando as escritas. Um DBA do time aponta um erro técnico nessa frase. Qual é, e por que a precisão importa em um documento de postmortem?

Ver resposta

✓ Resposta: O lock está errado. CREATE INDEX sem CONCURRENTLY adquire um ShareLock na tabela. O AccessShareLock citado é o lock mais fraco do PostgreSQL — o que um simples SELECT toma, e que conflita apenas com AccessExclusiveLock. Ele não bloqueia escrita nenhuma; se fosse esse o lock adquirido, o incidente descrito não teria acontecido.

O ShareLock, sim, conflita com RowExclusiveLock — o lock que INSERT, UPDATE e DELETE tomam. É exatamente essa incompatibilidade que travou as escritas por 29 minutos, deixando as leituras passarem (o que explica a aplicação responder a consultas enquanto o checkout falhava). O CREATE INDEX CONCURRENTLY usa ShareUpdateExclusiveLock, que não conflita com escrita — daí funcionar em produção, ao custo de dois passos de varredura e da restrição de não rodar dentro de um bloco de transação, corretamente registrada no documento.

A precisão importa porque um postmortem é um documento de referência, não um relato. Ele é publicado no wiki de engenharia para que "toda a organização possa aprender", e a ação corretiva nº 5 é justamente documentar migrations seguras em tabelas grandes — provavelmente citando este texto. O erro se propaga: alguém no futuro procura por AccessShareLock, encontra a documentação do PostgreSQL dizendo que ele não bloqueia escritas, e conclui que a explicação não fecha. O documento perde autoridade inteiro por causa de um nome trocado.

Há um efeito mais sutil, e é o que mais custa. A cultura de não-culpa depende de o postmortem ser tecnicamente confiável — é o que o distingue de um relatório defensivo. Quando alguém encontra um erro factual, a leitura natural passa a ser "escreveram rápido para fechar o assunto". O rigor técnico é o que sustenta a credibilidade do processo; sem ele, o blameless vira formalidade.

Exercício 5

O guia do facilitador manda usar os "5 Porquês" e adverte: "evite parar no primeiro erro humano — pergunte: o que no sistema tornou esse erro possível?". Examine a cadeia de exemplo do próprio guia à luz dessa regra. Ela obedece à própria advertência?

Ver resposta

✓ Resposta: Parcialmente. O terceiro elo é "Desenvolvedor não conhecia a diferença" — um erro humano puro. A cadeia tem o mérito de não parar ali: segue para "não há documentação nem treinamento" e chega a "nunca foi priorizado formalmente". Mas ao passar pela pessoa, ela estreita o campo de investigação a uma única direção — conhecimento — e é isso que determina as ações que saem da reunião.

Repare no efeito: quatro das seis ações corretivas são sobre informar gente (documentar no wiki, treinar no onboarding, criar checklist, revisar PR com mais atenção). São ações que dependem de as pessoas lembrarem, no momento certo, de algo que aprenderam meses antes.

Perguntar "o que no sistema tornou esse erro possível?" abre caminhos que a cadeia não percorreu, e nenhum deles passa por conhecimento individual: por que uma migration perigosa alcança a produção sem verificação automática?; por que o staging tem 50 mil linhas onde a produção tem 23 milhões?; por que não havia alerta de lock prolongado?. Cada um leva a um controle que funciona mesmo com a pessoa cansada, nova ou distraída — que é o único tipo de controle com o qual se pode contar às 14h23 de uma terça.

O documento, aliás, chega a essa conclusão sozinho, na seção "Aprendizados para o Time": a ação de maior impacto não é o lint, e sim a paridade de dados entre staging e produção, "sem ela, nenhuma quantidade de revisão manual detectará problemas de escala". E menciona um incidente similar em agosto de 2024, com ALTER TABLE — a reincidência que confirma o diagnóstico sistêmico.

Só que a tabela de ações não reflete essa prioridade: a paridade de staging aparece como um item entre seis, com o prazo mais longo de todos. É a falha silenciosa mais comum do postmortem — a análise identifica a causa sistêmica e o plano de ação trata os sintomas, porque documentar e treinar são baratos e imediatos, enquanto reconstruir o staging é caro e demorado. Daí a orientação do guia de preferir ações preventivas automatizadas às paliativas: sem ela, o próximo postmortem de migration já está escrito.

Comentários

Mais em DevOps

EKS: Kubernetes Gerenciado na AWS
EKS: Kubernetes Gerenciado na AWS

Kubernetes gerenciado sem a responsabilidade de operar o plano de controle: o…

Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento
Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento

A disciplina que nasce quando o time de plataforma vira gargalo: o conceito de…

Variáveis de Ambiente e Secrets em Pipelines
Variáveis de Ambiente e Secrets em Pipelines

A distinção entre configuração e segredo, e o que fazer com cada um: os três…