nexus-core-v3 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/agents/README.md +133 -0
- package/agents/_protocol.md +107 -0
- package/agents/analyst.md +138 -0
- package/agents/architect.md +146 -0
- package/agents/data-engineer.md +170 -0
- package/agents/dev.md +134 -0
- package/agents/devops.md +141 -0
- package/agents/nexus-master.md +147 -0
- package/agents/pm.md +133 -0
- package/agents/po.md +138 -0
- package/agents/qa.md +192 -0
- package/agents/sm.md +122 -0
- package/agents/squad-creator.md +121 -0
- package/agents/ux-design-expert.md +165 -0
- package/artifact-manifest.json +903 -0
- package/bin/nexus.mjs +37 -0
- package/checklists/README.md +49 -0
- package/checklists/architect-checklist.md +47 -0
- package/checklists/change-checklist.md +61 -0
- package/checklists/db-predeploy-checklist.md +57 -0
- package/checklists/design-quality-checklist.md +57 -0
- package/checklists/discovery-checklist.md +36 -0
- package/checklists/foundation-checklist.md +39 -0
- package/checklists/launch-checklist.md +39 -0
- package/checklists/pm-checklist.md +48 -0
- package/checklists/po-master-checklist.md +64 -0
- package/checklists/reality-check-checklist.md +49 -0
- package/checklists/story-dod-checklist.md +52 -0
- package/checklists/story-draft-checklist.md +36 -0
- package/dist/bin/dashboard.html +279 -0
- package/dist/bin/nexus.mjs +20008 -0
- package/dist/constitution.yaml +76 -0
- package/knowledge/README.md +57 -0
- package/knowledge/architecture/architectural-styles-map.md +182 -0
- package/knowledge/architecture/design-patterns-gof.md +192 -0
- package/knowledge/architecture/distributed-patterns-cheatsheet.md +201 -0
- package/knowledge/architecture/saas-subscription-blueprint.md +355 -0
- package/knowledge/architecture/system-design-tradeoffs.md +231 -0
- package/knowledge/architecture/t3-fullstack-typesafe-stack.md +273 -0
- package/knowledge/copy/landing-copy-that-converts.md +168 -0
- package/knowledge/data/postgres-indexing-and-tuning.md +263 -0
- package/knowledge/data/schema-modeling-decisions.md +273 -0
- package/knowledge/data/supabase-rls-patterns.md +316 -0
- package/knowledge/data/zero-downtime-migrations.md +308 -0
- package/knowledge/devops/cicd-pipeline-best-practices.md +318 -0
- package/knowledge/devops/production-dockerfile.md +283 -0
- package/knowledge/devops/twelve-factor-app.md +398 -0
- package/knowledge/engineering/clean-code-principles.md +429 -0
- package/knowledge/engineering/effective-code-review.md +204 -0
- package/knowledge/engineering/testing-strategy-beyond-unit.md +307 -0
- package/knowledge/governance/risk-matrix.md +56 -0
- package/knowledge/integration/mcp-server-selection-matrix.md +235 -0
- package/knowledge/marketing/copy-que-converte.md +43 -0
- package/knowledge/marketing/funil-e-jornada.md +36 -0
- package/knowledge/negocios/proposta-vencedora.md +38 -0
- package/knowledge/negocios/roi-e-unit-economics.md +46 -0
- package/knowledge/pipeline/1-descobrir.md +26 -0
- package/knowledge/pipeline/2-estrategizar.md +26 -0
- package/knowledge/pipeline/3-estruturar.md +27 -0
- package/knowledge/pipeline/4-construir.md +27 -0
- package/knowledge/pipeline/5-endurecer.md +28 -0
- package/knowledge/pipeline/6-lancar.md +27 -0
- package/knowledge/pipeline/7-operar.md +27 -0
- package/knowledge/security/lgpd-conformidade-basica.md +35 -0
- package/knowledge/security/owasp-secure-coding-gates.md +220 -0
- package/knowledge/security/owasp-top10-threat-assessment.md +287 -0
- package/knowledge/security/threat-modeling-stride.md +34 -0
- package/knowledge/web-craft/a11y-audit-checklist.md +251 -0
- package/knowledge/web-craft/accessible-component-patterns.md +383 -0
- package/knowledge/web-craft/anti-ai-look.md +114 -0
- package/knowledge/web-craft/design-system-from-code.md +195 -0
- package/knowledge/web-craft/intrinsic-css-layout.md +420 -0
- package/knowledge/web-craft/style-cloning.md +185 -0
- package/knowledge/web-craft/visual-polish-review.md +183 -0
- package/package.json +55 -0
- package/runbooks/campanha-de-conteudo.md +36 -0
- package/runbooks/feature-em-projeto-existente.md +37 -0
- package/runbooks/mvp-startup.md +38 -0
- package/runbooks/resposta-a-incidente.md +37 -0
- package/squads/exemplo-conteudo/agents/editor-chefe.md +48 -0
- package/squads/exemplo-conteudo/agents/pesquisador.md +44 -0
- package/squads/exemplo-conteudo/agents/redator.md +45 -0
- package/squads/exemplo-conteudo/knowledge/estilo-editorial.md +21 -0
- package/squads/exemplo-conteudo/squad.yaml +19 -0
- package/squads/exemplo-conteudo/tasks/pesquisar-fontes.md +26 -0
- package/squads/exemplo-conteudo/tasks/planejar-pauta.md +27 -0
- package/squads/exemplo-conteudo/tasks/redigir-artigo.md +26 -0
- package/squads/exemplo-conteudo/tasks/revisar-artigo.md +27 -0
- package/squads/marketing/agents/analista.md +56 -0
- package/squads/marketing/agents/chefe-marketing.md +65 -0
- package/squads/marketing/agents/conteudo.md +55 -0
- package/squads/marketing/agents/copy.md +55 -0
- package/squads/marketing/agents/growth.md +56 -0
- package/squads/marketing/agents/social.md +55 -0
- package/squads/marketing/squad.yaml +17 -0
- package/squads/marketing/tasks/aprovar-campanha.md +43 -0
- package/squads/negocios/agents/chefe-negocios.md +65 -0
- package/squads/negocios/agents/financas-roi.md +55 -0
- package/squads/negocios/agents/suporte.md +55 -0
- package/squads/negocios/agents/vendas-proposta.md +56 -0
- package/squads/negocios/squad.yaml +17 -0
- package/squads/negocios/tasks/aprovar-proposta.md +40 -0
- package/squads/security/agents/appsec-reviewer.md +59 -0
- package/squads/security/agents/chefe-seguranca.md +65 -0
- package/squads/security/agents/compliance-auditor.md +60 -0
- package/squads/security/agents/threat-modeler.md +60 -0
- package/squads/security/squad.yaml +20 -0
- package/squads/security/tasks/aprovar-gate-seguranca.md +42 -0
- package/squads/security/tasks/emitir-parecer-conformidade.md +42 -0
- package/tasks/README.md +72 -0
- package/tasks/accessibility-wcag-checklist.md +69 -0
- package/tasks/advanced-elicitation.md +42 -0
- package/tasks/analyze-performance.md +54 -0
- package/tasks/analyze-project-structure.md +59 -0
- package/tasks/apply-qa-fixes.md +57 -0
- package/tasks/architect-analyze-impact.md +62 -0
- package/tasks/archive-squad.md +52 -0
- package/tasks/audit-codebase.md +53 -0
- package/tasks/build-component.md +61 -0
- package/tasks/calculate-roi.md +63 -0
- package/tasks/ci-cd-configuration.md +51 -0
- package/tasks/collect-visual-evidence.md +62 -0
- package/tasks/compose-molecule.md +57 -0
- package/tasks/consolidate-patterns.md +54 -0
- package/tasks/create-brownfield-prd.md +54 -0
- package/tasks/create-competitor-analysis.md +42 -0
- package/tasks/create-deep-research-prompt.md +62 -0
- package/tasks/create-doc.md +62 -0
- package/tasks/create-epic.md +49 -0
- package/tasks/create-front-end-spec.md +56 -0
- package/tasks/create-migration-plan.md +57 -0
- package/tasks/create-next-story.md +66 -0
- package/tasks/create-prd.md +53 -0
- package/tasks/create-project-brief.md +47 -0
- package/tasks/create-rls-policies.md +59 -0
- package/tasks/create-schema.md +57 -0
- package/tasks/create-service.md +55 -0
- package/tasks/create-squad.md +100 -0
- package/tasks/create-suite.md +62 -0
- package/tasks/db-apply-migration.md +56 -0
- package/tasks/db-domain-modeling.md +57 -0
- package/tasks/db-dry-run.md +50 -0
- package/tasks/db-env-check.md +57 -0
- package/tasks/db-load-csv.md +54 -0
- package/tasks/db-policy-apply.md +58 -0
- package/tasks/db-rollback.md +51 -0
- package/tasks/db-run-sql.md +61 -0
- package/tasks/db-seed.md +52 -0
- package/tasks/db-smoke-test.md +51 -0
- package/tasks/db-snapshot.md +48 -0
- package/tasks/db-verify-order.md +49 -0
- package/tasks/deliberate.md +46 -0
- package/tasks/design-indexes.md +59 -0
- package/tasks/dev-develop-story.md +61 -0
- package/tasks/document-project.md +59 -0
- package/tasks/execute-checklist.md +57 -0
- package/tasks/execute-epic-plan.md +52 -0
- package/tasks/execute-subtask.md +51 -0
- package/tasks/extend-pattern.md +63 -0
- package/tasks/extend-squad.md +60 -0
- package/tasks/extract-patterns.md +64 -0
- package/tasks/extract-tokens.md +59 -0
- package/tasks/facilitate-brainstorming-session.md +42 -0
- package/tasks/generate-ai-frontend-prompt.md +57 -0
- package/tasks/generate-documentation.md +60 -0
- package/tasks/generate-migration-strategy.md +57 -0
- package/tasks/generate-shock-report.md +56 -0
- package/tasks/mcp-management.md +66 -0
- package/tasks/orchestrate.md +50 -0
- package/tasks/perform-market-research.md +42 -0
- package/tasks/plan-create-context.md +57 -0
- package/tasks/plan-create-implementation.md +58 -0
- package/tasks/po-close-story.md +60 -0
- package/tasks/po-manage-story-backlog.md +59 -0
- package/tasks/po-pull-story.md +60 -0
- package/tasks/po-sync-story.md +59 -0
- package/tasks/pr-automation.md +50 -0
- package/tasks/pre-push-quality-gate.md +54 -0
- package/tasks/push.md +53 -0
- package/tasks/qa-browser-console-check.md +52 -0
- package/tasks/qa-create-fix-request.md +58 -0
- package/tasks/qa-evidence-requirements.md +55 -0
- package/tasks/qa-false-positive-detection.md +55 -0
- package/tasks/qa-fix-issues.md +55 -0
- package/tasks/qa-gate.md +53 -0
- package/tasks/qa-migration-validation.md +58 -0
- package/tasks/qa-nfr-assess.md +45 -0
- package/tasks/qa-review-story.md +56 -0
- package/tasks/qa-risk-profile.md +45 -0
- package/tasks/qa-security-checklist.md +64 -0
- package/tasks/qa-test-design.md +47 -0
- package/tasks/qa-trace-requirements.md +48 -0
- package/tasks/release-management.md +53 -0
- package/tasks/repository-cleanup.md +61 -0
- package/tasks/route.md +44 -0
- package/tasks/run-tests.md +50 -0
- package/tasks/security-audit.md +54 -0
- package/tasks/setup-database.md +60 -0
- package/tasks/setup-design-system.md +60 -0
- package/tasks/shard-doc.md +60 -0
- package/tasks/spec-assess-complexity.md +55 -0
- package/tasks/spec-critique.md +64 -0
- package/tasks/spec-gather-requirements.md +48 -0
- package/tasks/spec-research-dependencies.md +42 -0
- package/tasks/spec-write-spec.md +50 -0
- package/tasks/test-as-user.md +52 -0
- package/tasks/ux-create-wireframe.md +54 -0
- package/tasks/ux-user-research.md +55 -0
- package/tasks/validate-next-story.md +61 -0
- package/tasks/validate-squad.md +55 -0
- package/tasks/verify-subtask.md +52 -0
- package/tasks/version-management.md +45 -0
- package/templates/README.md +47 -0
- package/templates/architecture-tmpl.md +115 -0
- package/templates/competitor-analysis-tmpl.md +87 -0
- package/templates/epic-tmpl.md +83 -0
- package/templates/front-end-spec-tmpl.md +110 -0
- package/templates/market-research-tmpl.md +98 -0
- package/templates/migration-plan-tmpl.md +92 -0
- package/templates/prd-tmpl.md +95 -0
- package/templates/project-brief-tmpl.md +100 -0
- package/templates/qa-verdict-tmpl.md +73 -0
- package/templates/rls-policies-tmpl.md +93 -0
- package/templates/schema-design-tmpl.md +107 -0
- package/templates/spec-tmpl.md +88 -0
- package/templates/squad/agent-dna-tmpl.md +72 -0
- package/templates/squad/chief-dna-tmpl.md +98 -0
- package/templates/squad/squad-task-tmpl.md +50 -0
- package/templates/squad/squad-yaml-tmpl.md +47 -0
- package/templates/story-tmpl.md +63 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: production-dockerfile
|
|
3
|
+
domain: devops
|
|
4
|
+
agents: [devops]
|
|
5
|
+
when: "ao escrever um Dockerfile destinado a produção"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Dockerfile de produção — imagens enxutas, seguras e cacheáveis
|
|
9
|
+
|
|
10
|
+
## O problema
|
|
11
|
+
|
|
12
|
+
Um Dockerfile que "funciona" na sua máquina quase nunca é um Dockerfile de produção.
|
|
13
|
+
O tell de um Dockerfile amador é reconhecível:
|
|
14
|
+
|
|
15
|
+
- **Roda como `root`** porque nunca declarou `USER` — qualquer RCE no app vira root no host.
|
|
16
|
+
- **Uma imagem de 1.2 GB** porque empacotou compilador, headers e cache do gerenciador de
|
|
17
|
+
pacotes junto com o binário de runtime.
|
|
18
|
+
- **`COPY . .` no topo** — toda mudança em qualquer arquivo (até um `README`) invalida o cache
|
|
19
|
+
de `npm install`/`pip install` e o build inteiro roda de novo.
|
|
20
|
+
- **`FROM node:latest`** — build não-reproduzível; a `latest` de amanhã não é a de hoje.
|
|
21
|
+
- **`ARG NPM_TOKEN` / `ENV AWS_SECRET=...`** — o segredo fica gravado para sempre numa layer e
|
|
22
|
+
aparece em `docker history`.
|
|
23
|
+
- **`RUN apt-get install ...`** em uma layer e `rm -rf /var/lib/apt/lists/*` em outra — a layer
|
|
24
|
+
intermediária ainda carrega o cache; limpar depois não remove nada do tamanho final.
|
|
25
|
+
|
|
26
|
+
Tudo isso passa no `docker build`. Nenhum disso passa numa revisão séria. O conhecimento abaixo
|
|
27
|
+
é a régua.
|
|
28
|
+
|
|
29
|
+
## O conhecimento
|
|
30
|
+
|
|
31
|
+
### 1. `USER` não-root antes do `CMD` — sempre
|
|
32
|
+
|
|
33
|
+
Container que sobe como `root` é o default do Docker, e é o default errado. Crie um usuário
|
|
34
|
+
dedicado e troque para ele **antes** do `CMD`/`ENTRYPOINT`.
|
|
35
|
+
|
|
36
|
+
**Ruim:**
|
|
37
|
+
```dockerfile
|
|
38
|
+
FROM node:22-slim
|
|
39
|
+
WORKDIR /app
|
|
40
|
+
COPY . .
|
|
41
|
+
RUN npm ci --omit=dev
|
|
42
|
+
CMD ["node", "server.js"] # roda como root (UID 0)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Bom:**
|
|
46
|
+
```dockerfile
|
|
47
|
+
FROM node:22-slim
|
|
48
|
+
WORKDIR /app
|
|
49
|
+
|
|
50
|
+
# cria grupo/usuário com UID/GID explícito (determinístico entre rebuilds)
|
|
51
|
+
RUN groupadd --system --gid 10001 app \
|
|
52
|
+
&& useradd --system --uid 10001 --gid app --no-create-home app
|
|
53
|
+
|
|
54
|
+
COPY --chown=app:app . .
|
|
55
|
+
RUN npm ci --omit=dev
|
|
56
|
+
|
|
57
|
+
USER 10001:10001 # numérico: o Kubernetes valida runAsNonRoot
|
|
58
|
+
CMD ["node", "server.js"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Por que UID/GID **numérico e explícito**: a doc oficial avisa que usuários criados sem UID
|
|
62
|
+
recebem um valor não-determinístico ("o próximo livre"), que pode mudar entre rebuilds. E com
|
|
63
|
+
`USER nonroot:nonroot` (nome), o `runAsNonRoot: true` do Kubernetes não consegue provar que o
|
|
64
|
+
UID != 0 — ele só confia em UID numérico. Use `USER 10001:10001`, não `USER app`.
|
|
65
|
+
|
|
66
|
+
### 2. Multi-stage build — deixe o compilador para trás
|
|
67
|
+
|
|
68
|
+
Ferramentas de build (compiladores, devDependencies, headers) não têm o que fazer na imagem de
|
|
69
|
+
runtime. Separe em estágios e copie só o artefato.
|
|
70
|
+
|
|
71
|
+
**Ruim** (toolchain inteiro no final, imagem gigante):
|
|
72
|
+
```dockerfile
|
|
73
|
+
FROM golang:1.25
|
|
74
|
+
WORKDIR /src
|
|
75
|
+
COPY . .
|
|
76
|
+
RUN go build -o /bin/app ./cmd/app
|
|
77
|
+
CMD ["/bin/app"] # carrega o Go SDK inteiro em produção
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Bom** (Go compila em `build`, runtime recebe só o binário):
|
|
81
|
+
```dockerfile
|
|
82
|
+
# --- estágio de build ---
|
|
83
|
+
FROM golang:1.25 AS build
|
|
84
|
+
WORKDIR /src
|
|
85
|
+
COPY go.mod go.sum ./
|
|
86
|
+
RUN go mod download
|
|
87
|
+
COPY . .
|
|
88
|
+
RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/app
|
|
89
|
+
|
|
90
|
+
# --- estágio de runtime ---
|
|
91
|
+
FROM gcr.io/distroless/static-debian12:nonroot
|
|
92
|
+
COPY --from=build /bin/app /app
|
|
93
|
+
USER 65532:65532
|
|
94
|
+
ENTRYPOINT ["/app"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`COPY --from=build` traz só `/bin/app`. O Go SDK e os intermediários ficam no estágio `build` e
|
|
98
|
+
nunca entram na imagem final.
|
|
99
|
+
|
|
100
|
+
### 3. Base slim/distroless + pin por digest (sha256)
|
|
101
|
+
|
|
102
|
+
| Base | Tamanho aprox. | Tem shell/gerenciador de pacotes? | Quando usar |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `node:22` / `python:3.13` | 1+ GB | sim, toolchain completa | só no estágio de build |
|
|
105
|
+
| `*-slim` (ex.: `node:22-slim`) | ~200 MB | sim, mínima | runtime de apps interpretados |
|
|
106
|
+
| `alpine:3.21` | < 6 MB + libs | sim (busybox, apk) | quando precisa de shell pequeno |
|
|
107
|
+
| `gcr.io/distroless/*:nonroot` | ~2–20 MB | **não** (sem shell, sem apt) | runtime de produção endurecido |
|
|
108
|
+
|
|
109
|
+
Distroless não tem shell — reduz superfície de ataque (sem `sh` para um atacante explorar) e já
|
|
110
|
+
vem com a variante `:nonroot` (UID 65532).
|
|
111
|
+
|
|
112
|
+
**Ruim** (tag móvel, build não-reproduzível):
|
|
113
|
+
```dockerfile
|
|
114
|
+
FROM node:latest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Bom** (tag + digest — imutável mesmo que o publisher reescreva a tag):
|
|
118
|
+
```dockerfile
|
|
119
|
+
FROM node:22.11-slim@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
O digest `@sha256:...` garante o mesmo bit-a-bit em qualquer máquina e em qualquer momento. Tag
|
|
123
|
+
sozinha (`node:22-slim`) ainda é alvo móvel: o registry pode reescrever para onde ela aponta.
|
|
124
|
+
|
|
125
|
+
### 4. Ordem das camadas para cache — deps antes do código
|
|
126
|
+
|
|
127
|
+
O Docker cacheia cada layer; quando uma instrução muda, **todas as seguintes** são reconstruídas.
|
|
128
|
+
Coloque o que muda raramente em cima (manifesto de dependências) e o que muda a cada commit
|
|
129
|
+
embaixo (código-fonte).
|
|
130
|
+
|
|
131
|
+
**Ruim** (qualquer mudança no código reinstala todas as deps):
|
|
132
|
+
```dockerfile
|
|
133
|
+
COPY . .
|
|
134
|
+
RUN npm ci --omit=dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Bom** (só `package*.json` invalida o `npm ci`; editar `src/` reaproveita o cache de install):
|
|
138
|
+
```dockerfile
|
|
139
|
+
COPY package.json package-lock.json ./
|
|
140
|
+
RUN npm ci --omit=dev
|
|
141
|
+
COPY . .
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Mesmo padrão em outras stacks:
|
|
145
|
+
```dockerfile
|
|
146
|
+
# Python
|
|
147
|
+
COPY requirements.txt .
|
|
148
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
149
|
+
COPY . .
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 5. `.dockerignore` — não mande lixo (nem segredo) pro contexto
|
|
153
|
+
|
|
154
|
+
Sem `.dockerignore`, `COPY . .` empacota `.git/`, `node_modules/`, `.env` — infla o contexto e
|
|
155
|
+
pode vazar segredo para dentro da imagem.
|
|
156
|
+
|
|
157
|
+
**`.dockerignore` mínimo:**
|
|
158
|
+
```dockerignore
|
|
159
|
+
.git
|
|
160
|
+
node_modules
|
|
161
|
+
.env
|
|
162
|
+
.env.*
|
|
163
|
+
*.log
|
|
164
|
+
Dockerfile
|
|
165
|
+
.dockerignore
|
|
166
|
+
**/__pycache__
|
|
167
|
+
dist
|
|
168
|
+
coverage
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 6. Limpeza na MESMA camada — sem cache vazando em layer
|
|
172
|
+
|
|
173
|
+
Layers são imutáveis e empilhadas: o que entra numa layer permanece no tamanho final mesmo que
|
|
174
|
+
uma layer **posterior** apague. Instale e limpe no **mesmo** `RUN`.
|
|
175
|
+
|
|
176
|
+
**Ruim** (o cache do apt já foi gravado na layer anterior; o `rm` posterior não reduz nada):
|
|
177
|
+
```dockerfile
|
|
178
|
+
RUN apt-get update && apt-get install -y curl ca-certificates
|
|
179
|
+
RUN rm -rf /var/lib/apt/lists/*
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Bom** (instala e limpa num único `RUN` → a layer já nasce enxuta):
|
|
183
|
+
```dockerfile
|
|
184
|
+
RUN apt-get update \
|
|
185
|
+
&& apt-get install -y --no-install-recommends curl ca-certificates \
|
|
186
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`--no-install-recommends` evita arrastar pacotes sugeridos que você não pediu. Em Alpine, o
|
|
190
|
+
equivalente é `apk add --no-cache`.
|
|
191
|
+
|
|
192
|
+
### 7. Nunca segredos em `ENV`/`ARG` — use `RUN --mount=type=secret`
|
|
193
|
+
|
|
194
|
+
`ENV` persiste na imagem e é lido por qualquer `docker inspect`. `ARG` fica visível em
|
|
195
|
+
`docker history`. Nenhum dos dois serve para token/credencial — a própria doc avisa: *"não é
|
|
196
|
+
recomendado usar build arguments para passar segredos; eles são visíveis em `docker history`"*.
|
|
197
|
+
|
|
198
|
+
**Ruim** (token gravado para sempre, vaza em `docker history`):
|
|
199
|
+
```dockerfile
|
|
200
|
+
ARG NPM_TOKEN
|
|
201
|
+
RUN npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN} && npm ci
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Bom** (BuildKit monta o segredo só durante o `RUN`; nunca entra em layer nem no cache):
|
|
205
|
+
```dockerfile
|
|
206
|
+
# syntax=docker/dockerfile:1
|
|
207
|
+
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
|
|
208
|
+
npm ci --omit=dev
|
|
209
|
+
```
|
|
210
|
+
Build:
|
|
211
|
+
```bash
|
|
212
|
+
DOCKER_BUILDKIT=1 docker build \
|
|
213
|
+
--secret id=npmrc,src=$HOME/.npmrc -t app .
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
O arquivo montado existe só no sistema de arquivos daquele `RUN` e desaparece depois — não fica
|
|
217
|
+
na imagem nem no build cache.
|
|
218
|
+
|
|
219
|
+
### 8. `HEALTHCHECK` — o orquestrador precisa saber se o app está vivo
|
|
220
|
+
|
|
221
|
+
Sem `HEALTHCHECK`, o Docker só sabe se o **processo** existe, não se o app **responde**. Um app
|
|
222
|
+
travado mas com PID vivo é reportado como saudável.
|
|
223
|
+
|
|
224
|
+
**Bom:**
|
|
225
|
+
```dockerfile
|
|
226
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
227
|
+
CMD curl -fsS http://localhost:3000/health || exit 1
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Defaults do Docker quando você só passa `CMD`: `--interval=30s`, `--timeout=30s`, `--retries=3`,
|
|
231
|
+
`--start-period=0s`. Ajuste `--start-period` para a janela de boot do app (evita marcar
|
|
232
|
+
`unhealthy` durante a partida). Se a base for distroless (sem `curl`), use o healthcheck do
|
|
233
|
+
orquestrador ou compile um probe binário no estágio de build.
|
|
234
|
+
|
|
235
|
+
### 9. `COPY` específico em vez de `COPY . .`
|
|
236
|
+
|
|
237
|
+
`COPY . .` arrasta tudo e invalida cache à toa. Copie só o que cada estágio precisa.
|
|
238
|
+
|
|
239
|
+
**Ruim:**
|
|
240
|
+
```dockerfile
|
|
241
|
+
COPY . .
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Bom:**
|
|
245
|
+
```dockerfile
|
|
246
|
+
COPY package.json package-lock.json ./
|
|
247
|
+
COPY src ./src
|
|
248
|
+
COPY public ./public
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Bônus: combine com `--chown` para já entregar a posse ao usuário não-root sem um `RUN chown`
|
|
252
|
+
extra (`COPY --chown=10001:10001 src ./src`).
|
|
253
|
+
|
|
254
|
+
## Checklist
|
|
255
|
+
|
|
256
|
+
Antes de marcar um Dockerfile como pronto para produção — qualquer "não" é bloqueio:
|
|
257
|
+
|
|
258
|
+
- [ ] Existe `USER <uid numérico>` (≠ 0) **antes** do `CMD`/`ENTRYPOINT`?
|
|
259
|
+
- [ ] O build é multi-stage e o estágio final **não** contém compilador/devDependencies?
|
|
260
|
+
- [ ] A base de runtime é `slim`/`distroless`/`alpine` (não a tag "gorda")?
|
|
261
|
+
- [ ] Todo `FROM` está pinado por **tag + `@sha256:` digest**?
|
|
262
|
+
- [ ] O manifesto de deps é copiado e instalado **antes** de copiar o código-fonte?
|
|
263
|
+
- [ ] Existe `.dockerignore` cobrindo `.git`, `node_modules`, `.env*`, build artifacts?
|
|
264
|
+
- [ ] Instalação de pacotes e limpeza (`rm -rf /var/lib/apt/lists/*`) estão no **mesmo** `RUN`?
|
|
265
|
+
- [ ] `--no-install-recommends` (apt) ou `--no-cache` (apk) presente?
|
|
266
|
+
- [ ] Zero segredos em `ENV`/`ARG`; tokens entram via `RUN --mount=type=secret`?
|
|
267
|
+
- [ ] Existe `HEALTHCHECK` apontando para um endpoint real de saúde?
|
|
268
|
+
- [ ] Os `COPY` são específicos (sem `COPY . .` no topo cacheável)?
|
|
269
|
+
- [ ] `COPY --chown` entrega a posse ao usuário não-root (sem `RUN chown` extra)?
|
|
270
|
+
|
|
271
|
+
## Tabela de decisão
|
|
272
|
+
|
|
273
|
+
| Situação | Faça |
|
|
274
|
+
|---|---|
|
|
275
|
+
| App compilado (Go, Rust, binário estático) | Multi-stage → final em `gcr.io/distroless/static-debian12:nonroot` |
|
|
276
|
+
| App interpretado (Node, Python) em produção | Multi-stage → final em `*-slim` ou `distroless/nodejs`/`distroless/python3`, `USER` numérico |
|
|
277
|
+
| Precisa de shell para debug em runtime | `*-slim` ou `alpine`; nunca a base "gorda" |
|
|
278
|
+
| Precisa instalar pacotes do sistema | Um único `RUN apt-get update && install --no-install-recommends ... && rm -rf /var/lib/apt/lists/*` |
|
|
279
|
+
| Precisa de token/credencial no build (npm, pip privado, git) | `RUN --mount=type=secret` + `docker build --secret` — nunca `ARG`/`ENV` |
|
|
280
|
+
| Build lento, reconstrói deps a cada commit | Copie manifesto (`package.json`/`requirements.txt`) e instale **antes** de `COPY . .` |
|
|
281
|
+
| Quer reprodutibilidade total entre máquinas/CI | Pin de `FROM` por `@sha256:` digest |
|
|
282
|
+
| Distroless sem `curl` mas precisa de HEALTHCHECK | Probe via orquestrador, ou compile um healthcheck binário no estágio de build |
|
|
283
|
+
| Config dinâmica em runtime (não-secreta) | `ENV` é ok; para secreta, injete em runtime (orquestrador/secrets manager), não no Dockerfile |
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: twelve-factor-app
|
|
3
|
+
domain: devops
|
|
4
|
+
agents: [devops]
|
|
5
|
+
when: "ao avaliar se uma aplicação está pronta para cloud / deploy contínuo"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Twelve-Factor App — o checklist enforçável de cloud-readiness
|
|
9
|
+
|
|
10
|
+
## O problema
|
|
11
|
+
|
|
12
|
+
"Pronto pra produção" costuma ser uma opinião. O dono do serviço jura que está, o deploy quebra
|
|
13
|
+
às 3h da manhã, e ninguém consegue dizer *qual* propriedade faltou. O Twelve-Factor (Adam Wiggins,
|
|
14
|
+
Heroku) existe para transformar essa opinião em **12 checagens binárias** — cada fator é uma
|
|
15
|
+
pergunta com resposta sim/não, e cada "não" é uma classe específica de incidente futuro.
|
|
16
|
+
|
|
17
|
+
Os tells de uma app que **não** está pronta:
|
|
18
|
+
|
|
19
|
+
- Credencial de banco hardcoded num `config/database.yml` commitado → vaza no primeiro repo público.
|
|
20
|
+
- "Funciona na minha máquina" porque dev usa SQLite e prod usa Postgres.
|
|
21
|
+
- App precisa de Apache/Tomcat instalado e configurado *fora* dela pra subir.
|
|
22
|
+
- `kill -9` na app perde jobs porque ninguém trata SIGTERM.
|
|
23
|
+
- Escalar = comprar máquina maior, porque o processo guarda sessão em memória local.
|
|
24
|
+
- Migração de banco roda por SSH manual com um script que vive fora do repo e desincroniza.
|
|
25
|
+
|
|
26
|
+
Este pack é a régua. Para cada fator: o **anti-padrão concreto** + a **correção**, com snippet real —
|
|
27
|
+
não prosa. A regra-mãe do Twelve-Factor sobre config (Fator III) serve de litmus test pro espírito
|
|
28
|
+
todo: *"whether the codebase could be made open source at any moment, without compromising any
|
|
29
|
+
credentials."* Se você não consegue abrir o repo agora, falhou.
|
|
30
|
+
|
|
31
|
+
## O conhecimento
|
|
32
|
+
|
|
33
|
+
### I — Codebase: um codebase, muitos deploys
|
|
34
|
+
|
|
35
|
+
*"If there are multiple codebases, it's not an app – it's a distributed system."*
|
|
36
|
+
*"Multiple apps sharing the same code is a violation of twelve-factor."*
|
|
37
|
+
|
|
38
|
+
Correlação **1:1** entre codebase e app. Um codebase (em git), N deploys (prod, staging, local). Código
|
|
39
|
+
compartilhado entre dois apps vira **biblioteca** puxada via dependency manager — não um diretório
|
|
40
|
+
copiado nem um submódulo que dois serviços editam.
|
|
41
|
+
|
|
42
|
+
| Ruim | Bom |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `api/` e `worker/` no mesmo repo, mas são dois apps com lifecycles diferentes → ainda 1 repo, OK; o problema é o inverso | 1 codebase = 1 app; dois apps que precisam do mesmo helper → extrai `lib-shared` versionada |
|
|
45
|
+
| `cp -r ../common ./vendor/common` em dois serviços | `common` publicado num registry privado; cada app declara `common@1.4.2` no manifest |
|
|
46
|
+
| Branch `production` com hotfixes que nunca voltam pra `main` | mesma codebase, deploys diferem só por *qual commit* + config |
|
|
47
|
+
|
|
48
|
+
> Deploy = instância rodando da app. Prod, staging e o laptop de cada dev são deploys do **mesmo**
|
|
49
|
+
> codebase, possivelmente em commits diferentes.
|
|
50
|
+
|
|
51
|
+
### II — Dependencies: declare e isole, explicitamente
|
|
52
|
+
|
|
53
|
+
*"A twelve-factor app never relies on implicit existence of system-wide packages. It declares all
|
|
54
|
+
dependencies, completely and exactly, via a dependency declaration manifest."*
|
|
55
|
+
*"Dependency declaration and isolation must always be used together – only one or the other is not
|
|
56
|
+
sufficient."*
|
|
57
|
+
|
|
58
|
+
E o item esquecido: *"Twelve-factor apps also do not rely on the implicit existence of any system
|
|
59
|
+
tools. Examples include shelling out to ImageMagick or curl."*
|
|
60
|
+
|
|
61
|
+
```dockerfile
|
|
62
|
+
# RUIM — depende de pacote do sistema e de uma tool implícita
|
|
63
|
+
FROM ubuntu:latest
|
|
64
|
+
RUN apt-get update && apt-get install -y python3 # versão imprevisível
|
|
65
|
+
COPY . /app
|
|
66
|
+
# em algum lugar do código: subprocess.run(["convert", "in.png", "out.jpg"]) # ImageMagick implícito
|
|
67
|
+
CMD ["python3", "/app/main.py"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```dockerfile
|
|
71
|
+
# BOM — runtime pinado, deps declaradas e isoladas, tool de sistema vendorizada
|
|
72
|
+
FROM python:3.12-slim
|
|
73
|
+
# ImageMagick declarado, não assumido
|
|
74
|
+
RUN apt-get update && apt-get install -y --no-install-recommends imagemagick=8:6.* \
|
|
75
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
76
|
+
WORKDIR /app
|
|
77
|
+
COPY requirements.txt .
|
|
78
|
+
RUN pip install --no-cache-dir -r requirements.txt # isolamento via imagem + venv implícito
|
|
79
|
+
COPY . .
|
|
80
|
+
CMD ["python", "main.py"]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Manifesto + isolamento juntos: `requirements.txt` + venv/container, `package.json` + `node_modules`
|
|
84
|
+
(com lockfile), `Gemfile` + Bundler, `go.mod`. Lockfile commitado é obrigatório — `^1.2.0` sem lock
|
|
85
|
+
não é declaração exata. Litmus: *um dev novo sobe o ambiente com só o runtime da linguagem + o
|
|
86
|
+
dependency manager*, nada de "ah, instala o ImageMagick na mão primeiro".
|
|
87
|
+
|
|
88
|
+
### III — Config: no ambiente, nunca no código
|
|
89
|
+
|
|
90
|
+
*"An app's config is everything that is likely to vary between deploys."*
|
|
91
|
+
*"The twelve-factor app stores config in environment variables."*
|
|
92
|
+
*"A litmus test... is whether the codebase could be made open source at any moment, without
|
|
93
|
+
compromising any credentials."*
|
|
94
|
+
|
|
95
|
+
Config = o que varia entre deploys (credenciais, handles de backing service, hostnames). **Não** é
|
|
96
|
+
config: roteamento interno, wiring de módulos — isso é código e fica no código.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# RUIM — segredo no código, "ambientes nomeados" (combinatorial explosion)
|
|
100
|
+
DATABASE = {
|
|
101
|
+
"development": "postgres://localhost/myapp_dev",
|
|
102
|
+
"production": "postgres://admin:S3cr3t@prod-db.internal/myapp", # vaza no git
|
|
103
|
+
}
|
|
104
|
+
db_url = DATABASE[os.environ.get("RAILS_ENV", "development")]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# BOM — env var granular, ortogonal, fora do código
|
|
109
|
+
db_url = os.environ["DATABASE_URL"] # falha alto se ausente; nunca tem default de prod
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Anti-padrão central: **agrupar config em ambientes nomeados** (`development`/`staging`/`production`).
|
|
113
|
+
*"This method does not scale cleanly... a combinatorial explosion of config which makes managing
|
|
114
|
+
deploys of the app very brittle."* Cada var é um controle **granular e ortogonal**: `DATABASE_URL`,
|
|
115
|
+
`REDIS_URL`, `STRIPE_KEY` — não um bloco `production`.
|
|
116
|
+
|
|
117
|
+
| Sinal | Veredito |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `grep -rE 'password\|secret\|api_key\|://.*:.*@' src/` retorna algo | FALHA — segredo no código |
|
|
120
|
+
| `.env` no `.gitignore`; `.env.example` commitado só com chaves vazias | OK |
|
|
121
|
+
| Código tem `if env == "production"` ramificando comportamento | Cheiro de ambiente nomeado |
|
|
122
|
+
|
|
123
|
+
### IV — Backing services: recursos anexáveis
|
|
124
|
+
|
|
125
|
+
*"The code for a twelve-factor app makes no distinction between local and third party services."*
|
|
126
|
+
*"...swap out a local MySQL database with one managed by a third party (such as Amazon RDS) without
|
|
127
|
+
any changes to the app's code."* — *"resources can be attached to and detached from deploys at will."*
|
|
128
|
+
|
|
129
|
+
Todo serviço consumido pela rede (Postgres, MySQL, RabbitMQ/Beanstalkd, SMTP/Postmark, Memcached/
|
|
130
|
+
Redis, S3) é um **recurso anexável**, acessado por um *handle* (URL) que vem da config (Fator III).
|
|
131
|
+
Trocar o Postgres local pelo RDS gerenciado = mudar uma env var, **zero deploy de código**.
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
# RUIM — host do broker hardcoded; trocar de provedor exige rebuild
|
|
135
|
+
services:
|
|
136
|
+
worker:
|
|
137
|
+
environment:
|
|
138
|
+
AMQP: "amqp://guest:guest@localhost:5672" # colado, e ainda com credencial
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# BOM — handle injetado; local e gerenciado são indistinguíveis pro código
|
|
143
|
+
services:
|
|
144
|
+
worker:
|
|
145
|
+
environment:
|
|
146
|
+
AMQP_URL: ${AMQP_URL} # amqp://localhost em dev, CloudAMQP em prod — mesma app
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Litmus: se o banco corromper, o ops troca o `DATABASE_URL` pra um réplica/backup e sobe — **sem
|
|
150
|
+
tocar no código**. Se precisa editar e rebuildar, o serviço não está "anexado", está soldado.
|
|
151
|
+
|
|
152
|
+
### V — Build, release, run: três estágios estritamente separados
|
|
153
|
+
|
|
154
|
+
*"The twelve-factor app uses strict separation between the build, release, and run stages."*
|
|
155
|
+
*"Releases are an append-only ledger and a release cannot be mutated once it is created. Any change
|
|
156
|
+
must create a new release."*
|
|
157
|
+
|
|
158
|
+
- **Build**: código → bundle executável (compila, instala deps, gera assets). Tolera complexidade.
|
|
159
|
+
- **Release**: build + config daquele deploy → release com **ID único** (`v100`, `2011-04-06-20:32:17`),
|
|
160
|
+
**imutável**.
|
|
161
|
+
- **Run**: executa a release no ambiente. Deve ser mínimo (quebra de madrugada, sem dev por perto).
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# RUIM — mutar código no servidor de run (sem release, sem rollback)
|
|
165
|
+
ssh prod 'cd /app && git pull && npm install && pm2 restart all'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# BOM — build artefato imutável, release com ID, run só seleciona a release
|
|
170
|
+
docker build -t myapp:$GIT_SHA . # BUILD
|
|
171
|
+
docker push registry/myapp:$GIT_SHA # release = imagem + config no orquestrador
|
|
172
|
+
kubectl set image deploy/myapp app=registry/myapp:$GIT_SHA # RUN seleciona a release
|
|
173
|
+
# rollback = reapontar pra release anterior; nunca "editar em produção"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Teste: dá pra fazer **rollback** pra release anterior por ID, instantâneo, sem rebuildar? Se não, os
|
|
177
|
+
estágios estão misturados. Não se altera código em runtime — *"changes cannot propagate back to the
|
|
178
|
+
build stage."*
|
|
179
|
+
|
|
180
|
+
### VI — Processes: stateless, share-nothing
|
|
181
|
+
|
|
182
|
+
*"Twelve-factor processes are stateless and share-nothing."*
|
|
183
|
+
*"Sticky sessions are a violation of twelve-factor and should never be used or relied upon."*
|
|
184
|
+
|
|
185
|
+
Memória/filesystem do processo = *"a brief, single-transaction cache"* — nunca assuma que algo
|
|
186
|
+
cacheado estará lá num request futuro (*"a future request will be served by a different process"*).
|
|
187
|
+
Qualquer dado que precisa persistir vai pra **backing service** (Fator IV).
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
// RUIM — estado na memória do processo + sticky session
|
|
191
|
+
const sessions = {}; // some no restart/scale
|
|
192
|
+
app.post("/login", (req, res) => { sessions[req.ip] = user; });
|
|
193
|
+
// e no LB: ip_hash; → gruda usuário no mesmo processo
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
// BOM — estado em datastore com expiração; processo é descartável
|
|
198
|
+
app.use(session({
|
|
199
|
+
store: new RedisStore({ client: redis }), // Redis/Memcached, com TTL
|
|
200
|
+
// qualquer instância serve qualquer request
|
|
201
|
+
}));
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Upload chega num processo, processamento num outro? Não pode depender do arquivo estar no disco local
|
|
205
|
+
— manda pro S3 (handle na config). Sticky session é o tell #1 de violação aqui.
|
|
206
|
+
|
|
207
|
+
### VII — Port binding: self-contained, exporta via porta
|
|
208
|
+
|
|
209
|
+
*"The web app exports HTTP as a service by binding to a port, and listening to requests coming in on
|
|
210
|
+
that port."* A app inclui o **webserver como biblioteca** (Tornado, Jetty, o `http` embutido) —
|
|
211
|
+
não roda *dentro* de um container Apache/Tomcat injetado em runtime.
|
|
212
|
+
|
|
213
|
+
```xml
|
|
214
|
+
<!-- RUIM — app é um .war que depende de um Tomcat externo instalado e configurado -->
|
|
215
|
+
<!-- deploy = copiar para /opt/tomcat/webapps/ e rezar pra versão bater -->
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
// BOM — a própria app faz bind na porta vinda da config
|
|
220
|
+
const port = process.env.PORT || 3000;
|
|
221
|
+
app.listen(port, () => console.log(`bound on :${port}`));
|
|
222
|
+
// em prod, a routing layer mapeia o hostname público -> esta porta
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
*"One app can become the backing service for another app, by providing the URL... as a resource
|
|
226
|
+
handle in the config for the consuming app."* — é o Fator IV fechando o círculo. Estende além de
|
|
227
|
+
HTTP (Redis protocol, XMPP). Teste: `docker run -p 8080:PORT myapp` responde sem nenhum webserver
|
|
228
|
+
externo? Então passa.
|
|
229
|
+
|
|
230
|
+
### VIII — Concurrency: escale horizontal via process model
|
|
231
|
+
|
|
232
|
+
*"In the twelve-factor app, processes are a first class citizen."*
|
|
233
|
+
*"Twelve-factor app processes should never daemonize or write PID files."*
|
|
234
|
+
|
|
235
|
+
Tipos de processo (web, worker) com workloads distintos; escala = **adicionar processos** (a *process
|
|
236
|
+
formation*), não engordar um. Lifecycle (output, crash recovery, shutdown) é do **process manager**
|
|
237
|
+
externo (systemd, Kubernetes, Foreman) — a app não se daemoniza.
|
|
238
|
+
|
|
239
|
+
```ini
|
|
240
|
+
# RUIM — um processo monolítico que se daemoniza e escreve PID
|
|
241
|
+
[program]
|
|
242
|
+
command=/app/server --daemon --pidfile=/var/run/app.pid # escala só vertical
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```procfile
|
|
246
|
+
# BOM — Procfile: tipos de processo, foreground, escala horizontal
|
|
247
|
+
web: gunicorn app:server --bind 0.0.0.0:$PORT
|
|
248
|
+
worker: celery -A app worker --loglevel=info
|
|
249
|
+
# escala: kubectl scale deploy/web --replicas=10 (mais processos, não maior)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Tells de violação: `--daemon`, `nohup ... &`, escrever `.pid`, ou "vamos aumentar a RAM da VM" como
|
|
253
|
+
única estratégia de escala. O processo roda em foreground; o orquestrador cuida do resto.
|
|
254
|
+
|
|
255
|
+
### IX — Disposability: startup rápido, shutdown gracioso, robusto à morte súbita
|
|
256
|
+
|
|
257
|
+
*"Processes shut down gracefully when they receive a SIGTERM signal from the process manager."*
|
|
258
|
+
*"Ideally, a process takes a few seconds from the time the launch command is executed until the
|
|
259
|
+
process is up and ready to receive requests or jobs."*
|
|
260
|
+
|
|
261
|
+
- **Startup rápido** (segundos) → scale e deploy ágeis.
|
|
262
|
+
- **SIGTERM gracioso**: web para de aceitar novas conexões e finaliza as em curso; worker *"returning
|
|
263
|
+
the current job to the work queue"*.
|
|
264
|
+
- **Robusto à morte súbita** (`kill -9`, falha de hardware): jobs **idempotentes/reentrantes** e
|
|
265
|
+
backend de fila que devolve o job no disconnect (RabbitMQ NACK, Beanstalkd auto-return).
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
// RUIM — ignora SIGTERM; orquestrador manda SIGKILL após o grace period e perde requests/jobs
|
|
269
|
+
// (nenhum handler) → conexões em curso morrem no meio
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
// BOM — drena conexões no SIGTERM
|
|
274
|
+
process.on("SIGTERM", async () => {
|
|
275
|
+
server.close(() => console.log("http drained")); // para de aceitar, termina em curso
|
|
276
|
+
await worker.close(); // devolve job atual à fila
|
|
277
|
+
process.exit(0);
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Teste: `docker stop` (= SIGTERM + grace) não derruba request em andamento nem perde job? E um
|
|
282
|
+
`kill -9` no worker faz o job voltar pra fila e ser reprocessado sem efeito colateral? Então passa.
|
|
283
|
+
|
|
284
|
+
### X — Dev/prod parity: mantenha dev, staging e prod o mais parecidos possível
|
|
285
|
+
|
|
286
|
+
*"The twelve-factor developer resists the urge to use different backing services between development
|
|
287
|
+
and production, even when adapters theoretically abstract away any differences."*
|
|
288
|
+
|
|
289
|
+
Três gaps a fechar:
|
|
290
|
+
|
|
291
|
+
| Métrica | App tradicional | Twelve-Factor App |
|
|
292
|
+
|---|---|---|
|
|
293
|
+
| Tempo entre deploys (time gap) | Semanas | Horas |
|
|
294
|
+
| Autores do código vs quem faz deploy (personnel gap) | Pessoas diferentes | Mesmas pessoas |
|
|
295
|
+
| Dev vs prod (tools gap) | Divergentes | O mais parecidos possível |
|
|
296
|
+
|
|
297
|
+
```yaml
|
|
298
|
+
# RUIM — SQLite em dev, Postgres em prod (mesmo "com ORM, é igual")
|
|
299
|
+
# dev: DATABASE_URL=sqlite:///dev.db
|
|
300
|
+
# prod: DATABASE_URL=postgres://...
|
|
301
|
+
# resultado: bug de tipo/locking só aparece em produção, depois do merge
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
# BOM — mesmo backing service em todo ambiente, via container
|
|
306
|
+
# docker-compose.yml (dev) sobe o MESMO Postgres da prod
|
|
307
|
+
services:
|
|
308
|
+
db:
|
|
309
|
+
image: postgres:16 # mesma major version que prod
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Docker/Compose tornaram "rodar o serviço de verdade localmente" barato — não há mais desculpa de
|
|
313
|
+
custo pra SQLite-em-dev. O risco de incompatibilidade *"undermines continuous deployment"*.
|
|
314
|
+
|
|
315
|
+
### XI — Logs: event stream pra stdout, sem gerenciar arquivo
|
|
316
|
+
|
|
317
|
+
*"A twelve-factor app never concerns itself with routing or storage of its output stream."*
|
|
318
|
+
Cada processo escreve seu event stream **unbuffered pra stdout**. Em dev, o dev vê no terminal; em
|
|
319
|
+
prod, *"each process' stream will be captured by the execution environment... and routed to one or
|
|
320
|
+
more final destinations."*
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
# RUIM — app gerencia arquivo, rotação, destino (responsabilidade que não é dela)
|
|
324
|
+
logging.basicConfig(
|
|
325
|
+
filename="/var/log/myapp/app.log", # disco local some no scale (Fator VI)
|
|
326
|
+
handlers=[RotatingFileHandler(...)], # rotação é trabalho do ambiente
|
|
327
|
+
)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
# BOM — escreve pra stdout, sem buffer; o ambiente coleta e roteia
|
|
332
|
+
import sys, logging
|
|
333
|
+
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
|
334
|
+
# (em Python, garanta unbuffered: PYTHONUNBUFFERED=1 no ambiente)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Tells: a app abre `FileHandler`, faz rotação, ou tenta mandar pro Datadog/ELK direto. Não — ela só
|
|
338
|
+
emite linhas em stdout (JSON estruturado é ótimo); coleta, agregação e roteamento são do execution
|
|
339
|
+
environment (fluentd, Loki, CloudWatch).
|
|
340
|
+
|
|
341
|
+
### XII — Admin processes: tarefas one-off no ambiente idêntico
|
|
342
|
+
|
|
343
|
+
*"One-off admin processes should be run in an identical environment as the regular long-running
|
|
344
|
+
processes of the app."* — contra a mesma release, mesma codebase e config. E o cuidado:
|
|
345
|
+
*"Admin code must ship with application code to avoid synchronization issues."*
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
# RUIM — migração roda por um script que vive FORA do repo, com conexão própria
|
|
349
|
+
ssh dba@prod 'psql $PROD -f /home/dba/manual_migrations/add_column.sql'
|
|
350
|
+
# desincroniza do código; usa env/credencial diferente do app
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
# BOM — migração versionada no repo, mesma release/config, mesmo isolamento de deps
|
|
355
|
+
kubectl run migrate --image=registry/myapp:$GIT_SHA --restart=Never \
|
|
356
|
+
--env-from=secretRef:myapp-config -- python manage.py migrate
|
|
357
|
+
# REPL idem: bundle exec rails console / python manage.py shell
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
A migração e o console usam o **mesmo bundle de deps** dos processos longos (se web é
|
|
361
|
+
`bundle exec thin start`, migração é `bundle exec rake db:migrate`). Tell: scripts de manutenção que
|
|
362
|
+
não estão no repo, ou que conectam com credenciais/imagem diferentes da app.
|
|
363
|
+
|
|
364
|
+
## Checklist
|
|
365
|
+
|
|
366
|
+
Marque cada item. Qualquer "não" é cloud-readiness incompleto.
|
|
367
|
+
|
|
368
|
+
- [ ] **I Codebase** — 1 repo = 1 app; código compartilhado é biblioteca versionada, não cópia/submódulo editado.
|
|
369
|
+
- [ ] **II Deps** — manifesto + lockfile commitados; runtime pinado; nenhuma tool de sistema (curl, ImageMagick) assumida.
|
|
370
|
+
- [ ] **III Config** — `grep` por segredo no código volta vazio; tudo em env var granular; repo poderia virar público agora.
|
|
371
|
+
- [ ] **IV Backing services** — trocar Postgres/SMTP/Redis = mudar uma env var, zero deploy de código.
|
|
372
|
+
- [ ] **V Build/release/run** — artefato imutável, release com ID, rollback por ID instantâneo; nada de `git pull` em prod.
|
|
373
|
+
- [ ] **VI Processes** — stateless; sessão/estado em datastore (Redis/Postgres); zero sticky session.
|
|
374
|
+
- [ ] **VII Port binding** — app faz bind na `$PORT` por conta própria; não precisa de Apache/Tomcat externo.
|
|
375
|
+
- [ ] **VIII Concurrency** — process types num Procfile; escala = mais réplicas; sem daemonizar nem PID file.
|
|
376
|
+
- [ ] **IX Disposability** — handler de SIGTERM drena conexões/jobs; startup em segundos; jobs idempotentes; sobrevive a `kill -9`.
|
|
377
|
+
- [ ] **X Dev/prod parity** — mesmo backing service (mesma major) em dev e prod; deploys em horas, não semanas.
|
|
378
|
+
- [ ] **XI Logs** — app escreve só pra stdout unbuffered; não abre arquivo, não rotaciona, não roteia.
|
|
379
|
+
- [ ] **XII Admin processes** — migração/console versionados no repo, contra a mesma release e config.
|
|
380
|
+
|
|
381
|
+
## Tabela de decisão
|
|
382
|
+
|
|
383
|
+
| Sintoma observado | Fator violado | Correção imediata |
|
|
384
|
+
|---|---|---|
|
|
385
|
+
| Credencial encontrada com `grep` no código-fonte | III | Mover pra env var; rotacionar o segredo vazado; `.env` no gitignore |
|
|
386
|
+
| `if env == "production"` ramificando lógica | III | Substituir por env vars granulares e ortogonais |
|
|
387
|
+
| Trocar de banco/provedor exige editar e rebuildar | IV | Acessar via handle/URL injetado pela config |
|
|
388
|
+
| Deploy é `ssh + git pull + restart` | V | Build de imagem imutável com ID; run seleciona release; rollback por ID |
|
|
389
|
+
| `kill -9` perde sessões ou jobs em andamento | VI + IX | Estado em Redis/Postgres; handler de SIGTERM; jobs idempotentes |
|
|
390
|
+
| LB com `ip_hash` / sticky session | VI | Session store compartilhado com TTL (Redis/Memcached) |
|
|
391
|
+
| App precisa de Tomcat/Apache pré-instalado pra subir | VII | Bundlar webserver como lib; bind em `$PORT` |
|
|
392
|
+
| Única forma de escalar é VM maior | VIII | Process types + réplicas horizontais; remover daemonização/PID |
|
|
393
|
+
| `--daemon`, `nohup &`, `.pid` no comando de start | VIII | Rodar em foreground sob process manager (systemd/k8s) |
|
|
394
|
+
| Bug só aparece em prod (SQLite em dev, Postgres em prod) | X | Mesmo backing service em todos os ambientes via container |
|
|
395
|
+
| App grava em `/var/log/app.log` e rotaciona | XI | Escrever em stdout unbuffered; deixar coleta/roteamento pro ambiente |
|
|
396
|
+
| Migração roda por script fora do repo / credencial própria | XII | Versionar no repo; rodar contra a mesma release/config/imagem |
|
|
397
|
+
| Sem lockfile; deps com `^`/`~` sem trava | II | Commitar lockfile; pinar runtime |
|
|
398
|
+
| Dois serviços editam o mesmo diretório de código copiado | I | Extrair pra biblioteca versionada no registry |
|