Pular para o conteúdo

Templates prontos

Todo template aqui está comentado explicando o porquê de cada decisão, não só o que fazer. Copiar sem entender é como o Dockerfile de 1,1 GB nasce.

ContainersCI/CDKubernetesObservabilidadeInfraestrutura como códigoSRE

Dockerfile — Node.js

Multi-stage com distroless e usuário não-root. Sai de ~1,1 GB para ~90 MB, com a camada de dependências separada para o cache sobreviver a mudanças de código.

Dockerfile
# Node.js em produção: multi-stage, distroless, usuário não-root.
# Resultado típico: ~90 MB contra ~1,1 GB de uma imagem node completa.
# ---------- dependências ----------
# Estágio separado só para node_modules: muda pouco, então o cache aproveita.
FROM node:22-bookworm-slim AS deps
WORKDIR /app
# Copiar só os manifests antes do código preserva o cache quando só o código muda.
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# ---------- build ----------
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---------- runtime ----------
# Distroless: sem shell, sem gerenciador de pacotes, superfície de ataque mínima.
# Para depurar, troque temporariamente por :nonroot-debug.
FROM gcr.io/distroless/nodejs22-debian12:nonroot AS runtime
WORKDIR /app
ENV NODE_ENV=production
# Sinal recebido pelo PID 1 encerra o processo: garanta que a aplicação trate SIGTERM.
ENV NODE_OPTIONS=--enable-source-maps
COPY --from=deps --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=build --chown=nonroot:nonroot /app/dist ./dist
COPY --from=build --chown=nonroot:nonroot /app/package.json ./
USER nonroot
EXPOSE 3000
# Sem shell na imagem: a forma exec é obrigatória (e é a correta de qualquer jeito,
# porque entrega os sinais direto ao processo, sem um shell no meio).
CMD ["dist/server.js"]

Dockerfile — Python

Instalação com uv, dependências fixadas pelo lock, usuário de sistema sem shell e cache de build montado.

Dockerfile
# Python em produção com uv: instalação rápida, dependências fixadas, não-root.
FROM python:3.13-slim-bookworm AS build
# uv resolve e instala ordens de grandeza mais rápido que pip.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never
# Camada de dependências separada do código: o cache sobrevive a mudança de código.
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# ---------- runtime ----------
FROM python:3.13-slim-bookworm AS runtime
WORKDIR /app
# Usuário sem privilégio, sem shell de login e sem diretório home gravável.
RUN groupadd --system --gid 10001 app \
&& useradd --system --uid 10001 --gid app --no-create-home --shell /usr/sbin/nologin app
COPY --from=build --chown=app:app /app /app
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
USER app
EXPOSE 8000
# Sem shell: sinais chegam direto ao Python, o que permite encerramento gracioso.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Dockerfile — Go

Binário estático em imagem scratch, com os certificados de CA e o fuso horário que quase todo mundo esquece de copiar.

Dockerfile
# Go em produção: binário estático em imagem scratch. Imagem final de poucos MB.
FROM golang:1.24-bookworm AS build
WORKDIR /src
# Módulos primeiro: o cache sobrevive a qualquer mudança de código.
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
# CGO desligado produz binário estático, requisito para rodar em scratch.
# -trimpath e -buildid= removem caminhos da máquina de build: reprodutibilidade.
ARG VERSION=dev
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w -buildid= -X main.version=${VERSION}" \
-o /out/app ./cmd/app
# ---------- runtime ----------
FROM scratch AS runtime
# scratch não tem nada: certificados de CA e fuso horário precisam vir de fora,
# senão qualquer chamada HTTPS falha com erro de certificado desconhecido.
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /out/app /app
# UID numérico: scratch não tem /etc/passwd para resolver nome de usuário.
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["/app"]

GitHub Actions — build, assinatura e deploy

Pipeline completo com cache no registry, scan, SBOM, assinatura com cosign e deploy autenticado por OIDC — nenhuma chave de longa duração em segredo.

.github/workflows/build-e-deploy.yml
# Build de imagem, assinatura e deploy na AWS sem nenhuma chave de longa duração.
# A autenticação usa OIDC: o GitHub prova a identidade do workflow, a AWS confia
# nessa prova e devolve credencial temporária.
name: build-e-deploy
on:
push:
branches: [main]
pull_request:
# Cancela execuções antigas do mesmo ref: em push seguido, só a última importa.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Permissão mínima no topo; cada job amplia só o que precisa.
permissions:
contents: read
env:
REGISTRY: ghcr.io
IMAGEM: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # necessário para a assinatura sem chave do cosign
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGEM }}
tags: |
type=sha,format=long
type=ref,event=branch
type=semver,pattern={{version}}
- id: push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Cache no registry sobrevive entre execuções — o cache local do runner não.
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Scan da imagem
if: github.event_name != 'pull_request'
uses: aquasecurity/[email protected]
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGEM }}@${{ steps.push.outputs.digest }}
severity: CRITICAL,HIGH
exit-code: '1'
ignore-unfixed: true # sem correção disponível, falhar o build não ajuda
- name: Assinar a imagem
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.push.outputs.digest }}
run: |
set -euo pipefail
cosign sign --yes "${REGISTRY}/${IMAGEM}@${DIGEST}"
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: producao # exige a aprovação configurada no ambiente
permissions:
contents: read
id-token: write # troca o token do GitHub por credencial da AWS
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
# A role confia só neste repositório e nesta branch — configure a
# condition sub no provedor OIDC, senão qualquer repo pode assumi-la.
role-to-assume: arn:aws:iam::123456789012:role/deploy-producao
aws-region: sa-east-1
- name: Promover o digest exato que foi testado
env:
DIGEST: ${{ needs.build.outputs.digest }}
run: |
set -euo pipefail
# Deploy por digest, nunca por tag: tag é mutável, digest não é.
aws ecs update-service \
--cluster producao \
--service api \
--force-new-deployment

GitHub Actions — Terraform com plan revisado

Plan no pull request com política e estimativa de custo, apply do plano salvo no merge. O que foi revisado é exatamente o que é aplicado.

.github/workflows/terraform.yml
# Plan no pull request, apply no merge. O plan é salvo como artefato e reutilizado
# no apply — isso garante que o que foi revisado é exatamente o que é aplicado.
name: terraform
on:
pull_request:
paths: ['infra/**']
push:
branches: [main]
paths: ['infra/**']
permissions:
contents: read
# Um apply por vez: infraestrutura não tolera execução concorrente.
concurrency:
group: terraform-producao
cancel-in-progress: false
defaults:
run:
working-directory: infra
jobs:
plan:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
pull-requests: write # para comentar o resultado no PR
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-plan
aws-region: sa-east-1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
terraform_wrapper: false
- run: terraform fmt -check -recursive
- run: terraform init -input=false
- run: terraform validate
- name: Política como código
run: |
set -euo pipefail
terraform plan -out=tfplan -input=false
terraform show -json tfplan > tfplan.json
checkov -f tfplan.json --framework terraform_plan --compact
- name: Estimativa de custo
run: infracost breakdown --path tfplan.json --format table
- uses: actions/upload-artifact@v4
with:
name: tfplan
path: infra/tfplan
retention-days: 5
apply:
needs: plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: producao
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-apply
aws-region: sa-east-1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
terraform_wrapper: false
- uses: actions/download-artifact@v4
with:
name: tfplan
path: infra
- run: terraform init -input=false
# Aplicar o plano salvo, e não gerar um novo: o que foi revisado é o que vai.
- run: terraform apply -input=false -auto-approve tfplan

Kubernetes — Deployment de produção

As três probes com papéis distintos, contexto de segurança restrito, espalhamento por zona, encerramento gracioso e PodDisruptionBudget.

deployment.yaml
# Deployment pronto para produção: probes distintas, limites conscientes,
# contexto de segurança restrito, encerramento gracioso e orçamento de disrupção.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
labels:
app.kubernetes.io/name: api
app.kubernetes.io/component: backend
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels:
app.kubernetes.io/name: api
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # nunca fica abaixo do número desejado durante o rollout
template:
metadata:
labels:
app.kubernetes.io/name: api
spec:
serviceAccountName: api
automountServiceAccountToken: false # a maioria dos Pods não fala com a API
terminationGracePeriodSeconds: 45 # precisa ser maior que o drain do preStop
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
# Espalha as réplicas entre zonas: uma zona cair não derruba o serviço.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: api
containers:
- name: api
# Digest, não tag: garante que o que rodou em homologação é o que roda aqui.
image: ghcr.io/exemplo/api@sha256:0000000000000000000000000000000000000000000000000000000000000000
ports:
- name: http
containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ['ALL']
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
# Sem limite de CPU de propósito: o limite causa throttling mesmo com
# CPU ociosa no nó. O request já garante a fatia mínima.
memory: 256Mi # igual ao request: classe Guaranteed, sem despejo
# startup protege partida lenta sem afrouxar a liveness.
startupProbe:
httpGet: { path: /healthz, port: http }
periodSeconds: 5
failureThreshold: 30 # até 150s para subir
# liveness só detecta processo travado. Nunca dependa de banco aqui:
# o banco cair reiniciaria todos os Pods em cascata.
livenessProbe:
httpGet: { path: /healthz, port: http }
periodSeconds: 10
failureThreshold: 3
# readiness decide se recebe tráfego. Aqui sim vale checar dependência.
readinessProbe:
httpGet: { path: /readyz, port: http }
periodSeconds: 5
failureThreshold: 2
lifecycle:
preStop:
# Dá tempo ao endpoint sair das tabelas de roteamento antes de o
# processo morrer. Sem isso, requisições caem no meio do rollout.
exec:
command: ['sleep', '15']
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
# Sistema de arquivos somente leitura exige um tmp gravável explícito.
- name: tmp
emptyDir: {}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
# Protege contra drain de nó e upgrade de cluster derrubando tudo de uma vez.
minAvailable: 2
selector:
matchLabels:
app.kubernetes.io/name: api

Kubernetes — segmentação com negação padrão

Nega tudo e libera o mínimo — incluindo o DNS, cuja ausência é o motivo número um de "a rede quebrou depois que apliquei NetworkPolicy".

networkpolicies.yaml
# Ponto de partida de segmentação: nega tudo, depois libera o necessário.
# Aplique a política de negação primeiro e só então as de liberação, sempre
# validando em ambiente de teste — ordem invertida derruba o namespace.
# 1. Nega todo tráfego de entrada e saída no namespace.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: producao
spec:
podSelector: {} # todos os Pods do namespace
policyTypes: [Ingress, Egress]
---
# 2. Libera DNS. Sem isto, nada resolve nome e tudo parece "rede quebrada".
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: producao
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# 3. Libera a entrada do ingress controller para a API.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-para-api
namespace: producao
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: api
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8080
---
# 4. Libera a saída da API para o banco, e só para ele.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-para-banco
namespace: producao
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: api
policyTypes: [Egress]
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: postgres
ports:
- protocol: TCP
port: 5432

Prometheus — alertas de SLO por taxa de consumo

Recording rules e alertas de burn rate em janelas múltiplas, separando o que acorda alguém do que vira ticket.

slo-rules.yaml
# Alertas de SLO por taxa de consumo do error budget, com janelas múltiplas.
# Substitui a família de alertas "taxa de erro acima de 5%", que dispara por
# ruído momentâneo e não diz nada sobre o compromisso com o usuário.
#
# SLO de exemplo: 99,9% das requisições sem erro, medido em 30 dias.
# Orçamento de falha: 0,1%.
groups:
- name: slo-api-disponibilidade
rules:
# ---- razões de erro pré-calculadas, uma por janela ----
# Recording rules deixam os alertas legíveis e baratos de avaliar.
- record: job:erros:razao_rate5m
expr: |
sum(rate(http_requests_total{job="api",code=~"5.."}[5m]))
/
sum(rate(http_requests_total{job="api"}[5m]))
- record: job:erros:razao_rate1h
expr: |
sum(rate(http_requests_total{job="api",code=~"5.."}[1h]))
/
sum(rate(http_requests_total{job="api"}[1h]))
- record: job:erros:razao_rate6h
expr: |
sum(rate(http_requests_total{job="api",code=~"5.."}[6h]))
/
sum(rate(http_requests_total{job="api"}[6h]))
- record: job:erros:razao_rate3d
expr: |
sum(rate(http_requests_total{job="api",code=~"5.."}[3d]))
/
sum(rate(http_requests_total{job="api"}[3d]))
# ---- consumo rápido: acorda alguém ----
# 14,4x esgota o orçamento de 30 dias em pouco mais de 2 dias.
# A janela curta em conjunto com a longa evita disparo por pico isolado.
- alert: ApiErrorBudgetQueimaRapido
expr: |
job:erros:razao_rate1h > (14.4 * 0.001)
and
job:erros:razao_rate5m > (14.4 * 0.001)
for: 2m
labels:
severity: pagina
slo: api-disponibilidade
annotations:
summary: "API consumindo error budget 14,4x mais rapido que o tolerado"
description: >-
Nesse ritmo o orcamento de 30 dias acaba em cerca de 2 dias.
Taxa de erro na ultima hora: {{ $value | humanizePercentage }}.
runbook_url: https://runbooks.exemplo.com/api/error-budget
# ---- consumo lento: vira ticket, não acorda ninguém ----
- alert: ApiErrorBudgetQueimaLento
expr: |
job:erros:razao_rate6h > (3 * 0.001)
and
job:erros:razao_rate3d > (1 * 0.001)
for: 1h
labels:
severity: ticket
slo: api-disponibilidade
annotations:
summary: "API vai estourar o error budget no fim do mes"
description: >-
Consumo sustentado acima do orcamento. Nao e emergencia,
mas precisa entrar na priorizacao desta semana.
runbook_url: https://runbooks.exemplo.com/api/error-budget
- name: slo-api-latencia
rules:
# Latência medida por histograma: o quantil vem do bucket, não da média.
# Média esconde exatamente a cauda que o usuário sente.
- alert: ApiLatenciaP99Alta
expr: |
histogram_quantile(
0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{job="api"}[5m]))
) > 1.5
for: 10m
labels:
severity: ticket
annotations:
summary: "p99 de latencia da API acima de 1,5s por 10 minutos"
runbook_url: https://runbooks.exemplo.com/api/latencia

Terraform — esqueleto de módulo reutilizável

Módulo tratado como interface: variáveis com validação que produz mensagem clara, tags obrigatórias mescladas e outputs mínimos.

modules/servico/*.tf
# Esqueleto de módulo Terraform tratado como interface pública.
# A regra: quem usa o módulo nunca deveria precisar ler o main.tf para entendê-lo.
#
# Estrutura sugerida em disco:
# modules/servico/
# main.tf <- recursos (este arquivo, primeira parte)
# variables.tf <- a interface de entrada (segunda parte)
# outputs.tf <- a interface de saída (terceira parte)
# README.md <- gerado por terraform-docs, nunca escrito à mão
# =====================================================================
# main.tf
# =====================================================================
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.70"
}
}
}
locals {
# Tags obrigatórias em toda a organização, mescladas com as do chamador.
# A alocação de custo depende disto — sem tag, o gasto fica sem dono.
tags = merge(
{
Name = var.nome
Ambiente = var.ambiente
Modulo = "servico"
GerenciadoPor = "terraform"
},
var.tags
)
# Nome derivado uma vez só: repetir interpolação espalha inconsistência.
nome_completo = "${var.nome}-${var.ambiente}"
}
resource "aws_ecs_service" "este" {
name = local.nome_completo
cluster = var.cluster_arn
task_definition = var.task_definition_arn
desired_count = var.replicas
# Deploy sem indisponibilidade: sobe antes de derrubar.
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
# Reverte sozinho se as verificações de saúde falharem após o deploy.
deployment_circuit_breaker {
enable = true
rollback = true
}
network_configuration {
subnets = var.subnet_ids
security_groups = [aws_security_group.este.id]
assign_public_ip = false
}
lifecycle {
# A contagem é gerenciada por autoscaling em produção; ignorar evita que
# todo apply devolva o serviço ao número inicial.
ignore_changes = [desired_count]
}
tags = local.tags
}
resource "aws_security_group" "este" {
name_prefix = "${local.nome_completo}-"
vpc_id = var.vpc_id
description = "Trafego do servico ${local.nome_completo}"
lifecycle {
# Cria o novo antes de destruir o antigo: evita janela sem grupo de segurança.
create_before_destroy = true
}
tags = local.tags
}
# =====================================================================
# variables.tf — as variáveis SÃO a documentação do módulo.
# Descrição e validação não são opcionais: transformam erro de uso em
# mensagem clara, em vez de um plan incompreensível.
# =====================================================================
variable "nome" {
description = "Nome do servico, usado como prefixo dos recursos criados."
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{1,30}$", var.nome))
error_message = "O nome deve comecar com letra minuscula e conter apenas letras minusculas, numeros e hifen (2 a 31 caracteres)."
}
}
variable "ambiente" {
description = "Ambiente de destino. Controla tags e politicas de retencao."
type = string
validation {
condition = contains(["dev", "homolog", "producao"], var.ambiente)
error_message = "Ambiente deve ser um de: dev, homolog, producao."
}
}
variable "vpc_id" {
description = "VPC onde o servico sera criado."
type = string
}
variable "subnet_ids" {
description = "Sub-redes privadas para as tarefas. Use ao menos duas zonas."
type = list(string)
validation {
condition = length(var.subnet_ids) >= 2
error_message = "Informe ao menos duas sub-redes, em zonas diferentes, para tolerar a queda de uma zona."
}
}
variable "cluster_arn" {
description = "ARN do cluster ECS que hospeda o servico."
type = string
}
variable "task_definition_arn" {
description = "ARN da task definition a ser executada."
type = string
}
variable "replicas" {
description = "Numero inicial de tarefas. Em producao o autoscaling assume depois."
type = number
default = 2
validation {
condition = var.replicas >= 1
error_message = "E preciso ao menos uma replica."
}
}
variable "tags" {
description = "Tags adicionais, mescladas as tags obrigatorias do modulo."
type = map(string)
default = {}
}
# =====================================================================
# outputs.tf — a outra metade da interface.
# Exponha o que o chamador precisa compor com outros módulos, não o
# objeto inteiro: isso amarraria você à implementação atual.
# =====================================================================
output "service_arn" {
description = "ARN do servico ECS criado."
value = aws_ecs_service.este.id
}
output "security_group_id" {
description = "Grupo de seguranca do servico, para liberar acesso a dependencias."
value = aws_security_group.este.id
}
output "nome_completo" {
description = "Nome derivado, util para nomear alarmes e paineis relacionados."
value = local.nome_completo
}

Postmortem sem culpado

Estrutura com linha do tempo, fatores contribuintes, "o que deu sorte" — a seção mais valiosa — e ações com dono, prazo e critério de verificação.

postmortem.md
# Postmortem: [título curto e factual do incidente]
> Este documento é **sem culpado**. O objetivo é entender o sistema — técnico e
> organizacional — que permitiu o incidente, não descobrir quem errou. Se em algum
> momento a resposta for "fulano deveria ter tido mais cuidado", a análise parou cedo
> demais: a pergunta seguinte é por que o sistema permitiu que o cuidado de uma pessoa
> fosse a única barreira.
| Campo | Valor |
| --- | --- |
| Data | AAAA-MM-DD |
| Duração | Xh Ymin (do início do impacto à mitigação) |
| Severidade | SEV1 / SEV2 / SEV3 |
| Autor | Nome |
| Revisores | Nomes |
| Status | Rascunho / Em revisão / Concluído |
## Impacto
Descreva **o que o usuário sentiu**, não o que aconteceu na infraestrutura.
- Quem foi afetado, e quantos.
- O que deixou de funcionar, do ponto de vista de quem usa.
- Impacto mensurável: requisições com erro, pedidos perdidos, receita, error budget
consumido.
## Resumo
Dois ou três parágrafos que uma pessoa de fora do time consegue entender. O que
quebrou, por quê, como foi resolvido.
## Linha do tempo
Todos os horários no mesmo fuso, com fuso declarado. Registre também o que foi
**tentado e não funcionou** — essa é a parte que ensina.
| Horário (UTC-3) | Evento |
| --- | --- |
| 14:02 | Deploy da versão `abc123` chega a 100% do tráfego |
| 14:07 | Taxa de erro sobe de 0,02% para 4% |
| 14:11 | Alerta `ApiErrorBudgetQueimaRapido` dispara |
| 14:13 | Plantão reconhece; abre canal de incidente |
| 14:20 | Hipótese: sobrecarga no banco. Descartada às 14:28 |
| 14:31 | Causa identificada: vazamento de conexões na versão nova |
| 14:34 | Rollback iniciado |
| 14:39 | Taxa de erro volta ao normal — impacto encerrado |
## Fatores contribuintes
Não existe "a" causa raiz. Liste os fatores que, juntos, permitiram o incidente.
1. **Técnico** — o que no código ou na configuração permitiu a falha.
2. **Detecção** — por que demorou para perceber. O alerta existia? Disparou tarde?
3. **Resposta** — o que atrasou a mitigação. Faltou runbook, acesso, contexto?
4. **Organizacional** — pressa, falta de revisão, conhecimento concentrado em uma
pessoa, ausência de ambiente de teste representativo.
## O que funcionou bem
Não é cortesia: reconhecer o que funcionou evita que a próxima rodada de mudanças
destrua uma defesa que estava valendo.
## O que deu sorte
O que impediu o incidente de ser pior por acaso, e não por projeto. Isso costuma ser
o item mais valioso do documento — cada linha aqui é um incidente futuro.
## Ações
Cada ação tem **dono nomeado**, **prazo** e **critério de verificação**. Ação sem essas
três coisas é uma intenção, não um plano.
| # | Ação | Tipo | Dono | Prazo | Como verificar |
| --- | --- | --- | --- | --- | --- |
| 1 | Limitar o pool de conexões e alertar em 80% de uso | Prevenir | | | Alerta dispara em teste de carga |
| 2 | Reduzir janela do canário de 10 min para 3 min | Detectar | | | Deploy seguinte já usa o novo valor |
| 3 | Escrever runbook de vazamento de conexões | Mitigar | | | Alguém de fora do time consegue seguir |
**Regra:** ações de prevenção que não cabem no próximo ciclo devem ser recusadas
explicitamente, com justificativa — não deixadas em aberto para sempre.
## Perguntas em aberto
O que ainda não sabemos e quem vai investigar.