Artigos aprofundados sobre a tecnologia que molda o que vem a seguir.

Engenharia Defensiva: Sistemas Que Resistem à Catástrofe

Padrões de engenharia defensiva que evitam desastres em produção: molly guards, modos dry-run, soft deletes e por que as caixas de confirmação falham.

Um botão vermelho de emergência protegido por uma tampa plástica transparente com dobradiça sobre um painel de aço

Um engenheiro júnior de uma fintech de porte médio rodou um script de migração de banco de dados numa sexta-feira à tarde. O script deveria limpar registros órfãos em um ambiente de staging. Em vez disso, conectou-se ao banco de produção e apagou 4,2 milhões de registros de transações de clientes. O backup? Com três dias de idade. A empresa passou as 72 horas seguintes em resposta total a incidente, reconciliando manualmente os registros a partir dos logs da processadora de pagamentos. O custo total passou de US$ 400 mil, somando horas de engenharia, créditos para clientes e os relatórios regulatórios.

O script não tinha nenhuma verificação de ambiente. Nenhuma flag de dry-run. Nenhum prompt de confirmação que mostrasse qual banco estava sendo acessado. A connection string vinha de uma variável de ambiente que, por acaso, estava apontando para produção no notebook do engenheiro, deixada assim numa sessão de debug duas semanas antes. Cada uma dessas falhas era evitável.

Por que as caixas "Tem certeza?" não funcionam

O mecanismo de segurança mais comum em software é a caixa de confirmação. Também é o mais inútil. Estudos sobre fadiga de confirmação mostram que, depois de encontrar esses avisos algumas vezes, os usuários clicam em "Tem certeza?" quase sempre sem ler nada. O aviso fica invisível, só mais um clique no caminho para aquilo que você já tinha decidido fazer.

O problema não é que confirmações sejam uma má ideia. É que confirmações genéricas não carregam informação nenhuma. "Tem certeza de que quer continuar?" não diz o que você está prestes a fazer. Compare com: "Você está prestes a apagar 4.217.893 linhas da tabela transactions em prod-us-east-1. Digite o nome do banco para confirmar." A segunda versão obriga você a ler de fato o que está acontecendo. Essa é a diferença entre um quebra-molas e um molly guard.

O que é um Molly Guard e por que ele importa

O termo "molly guard" vem de uma cobertura plástica física colocada sobre o Big Red Button dos mainframes para evitar desligamentos acidentais. Reza a lenda que o nome homenageia a filha pequena de um programador, Molly, que vivia apertando o botão. Em software, um molly guard é qualquer mecanismo que torna ações destrutivas difíceis de executar por acidente, mantendo-as possíveis quando feitas de propósito.

O pacote Linux molly-guard faz exatamente isso. Ele intercepta os comandos shutdown, reboot e halt em sessões SSH e pede que você digite o hostname da máquina que está prestes a desligar. Você não consegue passar por ali no piloto automático: precisa provar que sabe em qual máquina está.

$ sudo reboot
Good grief! You're on a remote SSH session to prod-web-03.
Please type the hostname to confirm: prod-web-03
# Compare with the useless version:
$ sudo reboot
Are you sure? (y/n): y
# *types y without reading, muscle memory*

O princípio é simples: a confirmação deve exigir uma informação que prove que o operador entende a ação. Digitar "y" não prova nada. Digitar o hostname, o nome da tabela ou a quantidade de registros afetados prova que você leu o aviso.

Modos Dry-Run: veja antes de disparar

Toda operação destrutiva deveria ter um modo dry-run. Não "deveria" como boa prática, e sim "deveria" como em você vai acabar se arrependendo de não ter um. Um dry run executa toda a lógica da operação, registra exatamente o que aconteceria e então para. Sem efeitos colaterais. Visibilidade total.

O padrão é simples de implementar. Veja um script de migração com dry-run embutido:

#!/bin/bash
set -euo pipefail
DRY_RUN=${DRY_RUN:-true}  # Default to dry-run!
DB_HOST=$(get_db_host)
DB_NAME=$(get_db_name)
echo "Target: $DB_HOST / $DB_NAME"
echo "Mode: $([ "$DRY_RUN" = true ] && echo 'DRY RUN' || echo 'LIVE')"
ROWS=$(psql -h "$DB_HOST" -d "$DB_NAME" -t -c \
"SELECT count(*) FROM orphaned_records WHERE created_at < now() - interval '90 days'")
echo "Records to delete: $ROWS"
if [ "$DRY_RUN" = true ]; then
echo "Dry run complete. Set DRY_RUN=false to execute."
exit 0
fi
# Require explicit confirmation for live runs
read -p "Type '$DB_NAME' to confirm deletion: " CONFIRM
if [ "$CONFIRM" != "$DB_NAME" ]; then
echo "Aborted."
exit 1
fi
psql -h "$DB_HOST" -d "$DB_NAME" -c \
"DELETE FROM orphaned_records WHERE created_at < now() - interval '90 days'"
echo "Deleted $ROWS records."

Repare em três coisas: o padrão é o dry-run (você precisa optar por destruir, não por deixar de destruir), o script mostra o banco de destino e a contagem de linhas antes de fazer qualquer coisa, e mesmo no modo real exige que você digite o nome do banco. São três camadas de defesa. Qualquer uma delas teria evitado o incidente que descrevi no começo.

Redes de segurança para migrações de banco de dados

Migrações de banco estão entre as operações de maior risco em qualquer pipeline de deploy. Muitas vezes são irreversíveis, rodam sobre estado compartilhado, e uma migração ruim pode derrubar a aplicação inteira. Mesmo assim, a maioria das equipes trata isso como mais uma etapa de deploy.

Aqui vai uma hierarquia de mecanismos de segurança, do básico ao à prova de balas:

  1. Verificações de ambiente — O script de migração confirma que está apontando para o ambiente pretendido antes de executar. Parece óbvio. Você ficaria surpreso com a frequência com que isso falta.
  2. Snapshots pré-migração — Criar automaticamente um snapshot do banco antes de qualquer migração. Se a migração falhar ou causar problemas, você restaura em minutos, não em horas.
  3. Guardas de contagem de linhas — Se uma migração afetar mais de N linhas, exigir confirmação explícita. Uma migração que toca milhões de linhas sem aviso quase sempre é um bug.
  4. Timeouts de statement — Configure timeouts agressivos nas queries de migração. Uma migração que roda por 45 minutos está travando tabelas e degradando a performance. Falhe rápido.
  5. Somente compatível com versões anteriores — Garanta que toda migração seja compatível com o código atualmente em produção. Isso significa nada de renomear colunas, nada de adicionar NOT NULL sem default, nada de remover colunas que ainda são referenciadas.
# Rails example using strong_migrations gem
class AddPhoneToUsers < ActiveRecord::Migration[7.1]
def change
# This will raise an error with a safe alternative:
# add_column :users, :phone, :string, null: false
#
# Instead, do it in two steps:
add_column :users, :phone, :string
# Then in a separate migration after backfill:
# change_column_null :users, :phone, false
end
end

Ferramentas como strong_migrations no Rails, squawk para PostgreSQL e skeema para MySQL pegam padrões perigosos antes que cheguem à produção. Se você não usa nada disso, está confiando na code review para pegar problemas sutis de migração, e revisores deixam passar coisas.

Soft deletes e janelas de desfazer

Hard deletes são um compromisso permanente feito num momento de certeza. O problema é que essa certeza costuma estar errada. Soft deletes, que marcam registros como apagados sem de fato removê-los, dão uma janela para se recuperar de erros.

-- Hard delete: gone forever
DELETE FROM projects WHERE id = 4872;
-- Soft delete: recoverable
UPDATE projects
SET deleted_at = now(),
deleted_by = 'user:priya'
WHERE id = 4872;
-- Recovery is trivial
UPDATE projects
SET deleted_at = NULL,
deleted_by = NULL
WHERE id = 4872;

O trade-off é real: soft deletes complicam as queries (você precisa de WHERE deleted_at IS NULL em todo lugar), aumentam o armazenamento e podem gerar confusão sobre o estado real dos dados. Mas para qualquer dado voltado ao usuário em que a exclusão acidental é possível, vale a pena. O GitHub não apaga de fato o seu repositório por 90 dias. O Slack mantém mensagens apagadas por questões de compliance. A lixeira do Gmail esvazia após 30 dias. Isso não é acidente: são decisões de engenharia deliberadas.

O mesmo princípio vale para a infraestrutura. Em vez de terminar uma instância EC2, pare-a primeiro. Em vez de derrubar um banco, renomeie-o para mydb_deleted_20260315 e marque um lembrete no calendário para de fato removê-lo em duas semanas. O custo de manter uma instância parada ou um banco renomeado por alguns dias é insignificante comparado ao custo de restaurar a partir de backup.

Segurança em deploys: canários, circuit breakers e rollbacks

Deploys são outra categoria de ação destrutiva que a maioria das equipes não trata com cuidado suficiente. Um deploy ruim pode derrubar a produção tão eficientemente quanto um banco apagado, e acontece com muito mais frequência.

A configuração mínima viável de segurança em deploys inclui:

  • Deploys canário — Direcione de 1% a 5% do tráfego para a nova versão. Se a taxa de erros disparar, faça rollback automaticamente antes que o raio de impacto cresça.
  • Gatilhos de rollback automático — Defina limites de taxa de erro, de latência e falhas em health checks que disparam o rollback automaticamente. Não dependa de alguém perceber às 2 da manhã.
  • Congelamento de deploys durante incidentes — Se existe um incidente ativo, bloqueie todos os deploys. A última coisa de que você precisa durante o combate ao fogo é alguém subindo mudanças não relacionadas.
  • Rollback com um clique — Voltar atrás deveria ser mais fácil do que seguir em frente. Se o seu rollback envolve dar SSH nos servidores e rodar comandos manuais, você não tem um processo de rollback.
# Kubernetes progressive delivery with Argo Rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
strategy:
canary:
steps:
- setWeight: 5
- pause: { duration: 5m }
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 25
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
# Auto-rollback if error rate exceeds 1%
abortScaleDownDelaySeconds: 30

O padrão aqui é o comprometimento progressivo. Você não vai de 0 a 100% em um passo só. Dá pequenos passos, verifica cada um e mantém a capacidade de recuar em cada etapa. É mais lento do que fazer deploy em todas as instâncias de uma vez, mas na primeira vez que ele pegar um deploy ruim antes de atingir todos os usuários, você vai agradecer cada minuto extra.

Prevenção arquitetural: tornando a coisa errada impossível

Os melhores mecanismos de segurança não pedem que você tome cuidado. Eles tornam estruturalmente impossível fazer a coisa errada. Essa é a diferença entre uma grade de proteção e uma placa de aviso.

  • Infraestrutura imutável — Se você não consegue dar SSH em servidores de produção, não consegue rodar comandos neles por acidente. Se os deploys são sempre instâncias novas a partir de uma imagem conhecida, não há como ter configuration drift.
  • Acesso com menor privilégio — Engenheiros não deveriam ter credenciais de banco de produção no notebook. Ponto final. Use ferramentas de acesso just-in-time que concedem credenciais temporárias com trilhas de auditoria.
  • Credenciais separadas por ambiente — Se staging e produção usam cofres de credenciais diferentes, você literalmente não consegue se conectar à produção por acidente com as ferramentas de staging.
  • Proteção contra exclusão — A AWS permite ativar termination protection em instâncias EC2 e deletion protection em bancos RDS. Ative para tudo que importa. É uma mudança de configuração de cinco segundos que elimina uma classe inteira de erros catastróficos.

Se alguém consegue destruir a produção acidentalmente com um único comando, o problema não é a pessoa: é o sistema que permitiu que um único comando destruísse a produção.

Já vi equipes responderem a incidentes de produção acrescentando mais documentação, mais checklists, mais treinamento. Isso ajuda, mas tudo depende de humanos perfeitos. Humanos não são perfeitos. A resposta melhor é mudar o sistema para que o erro não possa acontecer em primeiro lugar, ou, se acontecer, para que o raio de impacto seja contido e a recuperação seja rápida.

Construindo uma cultura de engenharia com segurança em primeiro lugar

Ferramentas e arquitetura importam, mas é a cultura que determina se elas realmente são implementadas. Equipes que tratam mecanismos de segurança como overhead ou burocracia vão pulá-los sob pressão de prazo, e a pressão de prazo é permanente.

O padrão mais eficaz que já vi é tratar mecanismos de segurança como requisito de engenharia de primeira classe, não como algo bom de ter. Toda operação destrutiva passa por uma revisão de design que pergunta especificamente: o que acontece se isso rodar no alvo errado? O que acontece se rodar duas vezes? O que acontece se rodar com dados desatualizados? Como desfazemos isso?

Post-mortems sem culpados já são o mínimo nesse ponto. Mas a prática menos comum que acho ainda mais importante é o pre-mortem. Antes de lançar uma mudança arriscada, reúna a equipe e pergunte: "Assuma que isso deu muito errado. O que aconteceu?" As pessoas são surpreendentemente boas em identificar modos de falha quando você enquadra a conversa como imaginação, e não como previsão. As falhas que elas identificam viram os mecanismos de segurança que você constrói.

Aquele engenheiro júnior que derrubou o banco de produção? Ele continua na empresa. Hoje é um dos maiores defensores da engenharia defensiva na equipe. O incidente não foi culpa dele: foi uma falha de sistema. E o sistema está bem mais difícil de quebrar agora.