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.
# 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 depsWORKDIR /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 buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .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 runtimeWORKDIR /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_modulesCOPY --from=build --chown=nonroot:nonroot /app/dist ./distCOPY --from=build --chown=nonroot:nonroot /app/package.json ./
USER nonrootEXPOSE 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.
# 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 /appENV 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 runtimeWORKDIR /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 appEXPOSE 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.
# Go em produção: binário estático em imagem scratch. Imagem final de poucos MB.
FROM golang:1.24-bookworm AS buildWORKDIR /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=devRUN --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/zoneinfoCOPY --from=build /out/app /app
# UID numérico: scratch não tem /etc/passwd para resolver nome de usuário.USER 10001:10001EXPOSE 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.
# 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' 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-deploymentGitHub 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.
# 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 tfplanKubernetes — 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 pronto para produção: probes distintas, limites conscientes,# contexto de segurança restrito, encerramento gracioso e orçamento de disrupção.apiVersion: apps/v1kind: Deploymentmetadata: name: api labels: app.kubernetes.io/name: api app.kubernetes.io/component: backendspec: 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/v1kind: PodDisruptionBudgetmetadata: name: apispec: # Protege contra drain de nó e upgrade de cluster derrubando tudo de uma vez. minAvailable: 2 selector: matchLabels: app.kubernetes.io/name: apiKubernetes — 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".
# 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/v1kind: NetworkPolicymetadata: name: default-deny-all namespace: producaospec: 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/v1kind: NetworkPolicymetadata: name: allow-dns namespace: producaospec: 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/v1kind: NetworkPolicymetadata: name: allow-ingress-para-api namespace: producaospec: 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/v1kind: NetworkPolicymetadata: name: allow-api-para-banco namespace: producaospec: podSelector: matchLabels: app.kubernetes.io/name: api policyTypes: [Egress] egress: - to: - podSelector: matchLabels: app.kubernetes.io/name: postgres ports: - protocol: TCP port: 5432Prometheus — 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.
# 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/latenciaTerraform — 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.
# 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: [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 quequebrou, 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çasdestrua 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 sero 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 essastrê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 recusadasexplicitamente, com justificativa — não deixadas em aberto para sempre.
## Perguntas em aberto
O que ainda não sabemos e quem vai investigar.