@nodefony/devkit 10.0.0-alpha.6 → 10.0.0-alpha.7
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/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/controllers/DevkitController.js +2 -2
- package/dist/nodefony/controllers/McpController.js +3 -3
- package/dist/nodefony/service/DevkitService.js +2 -2
- package/package.json +8 -8
- package/skills/nodefony-add-crud/SKILL.md +9 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +9 -0
- package/skills/nodefony-add-service/SKILL.md +9 -0
- package/skills/nodefony-browser/SKILL.md +19 -16
- package/skills/nodefony-dev/SKILL.md +273 -0
- package/skills/nodefony-dev/scripts/docs.mjs +544 -0
- package/skills/nodefony-devops/SKILL.md +198 -0
- package/skills/nodefony-devops/references/compose.md +125 -0
- package/skills/nodefony-devops/references/frontal.md +119 -0
- package/skills/nodefony-devops/references/image.md +136 -0
- package/skills/nodefony-devops/references/kubernetes.md +189 -0
- package/skills/nodefony-devops/references/podman.md +103 -0
- package/skills/nodefony-devops/references/secrets.md +108 -0
- package/skills/nodefony-devops/references/variables.md +174 -0
- package/skills/nodefony-migrate-schema/SKILL.md +29 -17
- package/skills/nodefony-protect-route/SKILL.md +9 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Kubernetes — ton Deployment
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante. Éditer en place.
|
|
4
|
+
|
|
5
|
+
## 1. Ce qui est rendu, et ce qui ne l'est pas
|
|
6
|
+
|
|
7
|
+
**Rendu à ton nom** : `deploy/migrate-job.yaml`, le travail qui joue les migrations. Son mode
|
|
8
|
+
d'emploi est en tête du fichier.
|
|
9
|
+
|
|
10
|
+
**Pas rendu** : le Deployment, le Service, l'Ingress, les Secret et ConfigMap. Tu les écris. Cette
|
|
11
|
+
page dit quoi y mettre ; le guide `kubernetes.md` du framework en donne un exemple complet.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
kubectl apply -f deploy/migrate-job.yaml # les migrations AVANT les nouveaux exemplaires
|
|
15
|
+
kubectl wait --for=condition=complete job/<nom> --timeout=300s
|
|
16
|
+
kubectl apply -f <ton-deployment>.yaml
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
L'ordre n'est pas négociable : un exemplaire neuf face à un schéma ancien écrit dans des colonnes
|
|
20
|
+
qui n'existent pas.
|
|
21
|
+
|
|
22
|
+
## 2. Le contexte de sécurité — politique Restricted
|
|
23
|
+
|
|
24
|
+
**Commence par lire `deploy/migrate-job.yaml` : son contexte de sécurité est déjà conforme.** Il
|
|
25
|
+
est le seul modèle juste que tu aies sous la main, et il a été écrit pour être recopié.
|
|
26
|
+
|
|
27
|
+
Six exigences, toutes portées par le **manifeste** — aucune n'est « héritée de l'image ». Ce que
|
|
28
|
+
l'image apporte, c'est de les rendre satisfaisables sans contorsion : elle déclare
|
|
29
|
+
`USER 1000:1000`, donc l'identifiant numérique existe ; elle n'écoute qu'au-dessus de 1024, donc
|
|
30
|
+
aucune capacité n'est nécessaire. Cinq ne coûtent rien à poser. **La sixième —
|
|
31
|
+
`readOnlyRootFilesystem` — demande les deux volumes**, et c'est la seule qui se travaille.
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
spec:
|
|
35
|
+
securityContext:
|
|
36
|
+
runAsNonRoot: true
|
|
37
|
+
runAsUser: 1000
|
|
38
|
+
runAsGroup: 1000
|
|
39
|
+
fsGroup: 1000
|
|
40
|
+
fsGroupChangePolicy: OnRootMismatch # c'est LUI qui donne le volume au bon porteur
|
|
41
|
+
seccompProfile: { type: RuntimeDefault }
|
|
42
|
+
containers:
|
|
43
|
+
- name: <app>
|
|
44
|
+
securityContext:
|
|
45
|
+
allowPrivilegeEscalation: false
|
|
46
|
+
readOnlyRootFilesystem: true
|
|
47
|
+
capabilities: { drop: ["ALL"] }
|
|
48
|
+
volumeMounts:
|
|
49
|
+
- { name: tmp, mountPath: /app/tmp }
|
|
50
|
+
- { name: var, mountPath: /app/var }
|
|
51
|
+
volumes:
|
|
52
|
+
- { name: tmp, emptyDir: { sizeLimit: 256Mi } }
|
|
53
|
+
- { name: var, emptyDir: {} }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- **`seccompProfile`** : la politique traite l'**absence** de profil comme une violation, pas comme
|
|
57
|
+
un défaut permissif. Aucun coût : rien ici n'appelle de fonction système exotique.
|
|
58
|
+
- **`readOnlyRootFilesystem: true`** : possible **uniquement** avec les deux volumes ci-dessus.
|
|
59
|
+
Sans eux, le conteneur ne démarre pas — l'application crée `tmp/` et `var/` au démarrage.
|
|
60
|
+
|
|
61
|
+
> ⚠️ **Un volume éphémère en mémoire compte dans la limite mémoire du conteneur.** Laisse
|
|
62
|
+
> `emptyDir: {}` sur disque, et pose une `sizeLimit` — un dossier temporaire sans borne est un
|
|
63
|
+
> moyen de faire évincer ton propre pod.
|
|
64
|
+
|
|
65
|
+
**Le rendre opposable, sinon ça ne prouve rien :**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted
|
|
69
|
+
kubectl apply -f <ton-deployment>.yaml # doit passer SANS avertissement
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Puis **débranche** : retire un `emptyDir`, réapplique. Le pod ne doit plus démarrer. Une politique
|
|
73
|
+
qu'on n'a jamais vue refuser quelque chose n'est pas une politique.
|
|
74
|
+
|
|
75
|
+
## 3. Les sondes
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
startupProbe:
|
|
79
|
+
{
|
|
80
|
+
httpGet: { path: /readyz, port: 5151 },
|
|
81
|
+
periodSeconds: 2,
|
|
82
|
+
failureThreshold: 30,
|
|
83
|
+
}
|
|
84
|
+
readinessProbe: { httpGet: { path: /readyz, port: 5151 }, periodSeconds: 5 }
|
|
85
|
+
livenessProbe: { httpGet: { path: /livez, port: 5151 }, periodSeconds: 10 }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **Pas de sonde de démarrage dédiée côté application** : `/readyz` en fait office. Dimensionne
|
|
89
|
+
`failureThreshold × periodSeconds` sur le **pire** temps de démarrage, pas le meilleur — c'est la
|
|
90
|
+
cause n° 1 des pods qui redémarrent en boucle avec des journaux muets.
|
|
91
|
+
- **`livenessProbe` sur `/livez`, jamais sur `/readyz`.** `/livez` reste à `200` pendant le drain :
|
|
92
|
+
un exemplaire qui draine est vivant. Sonder `/readyz` en vivacité le ferait **tuer** au milieu de
|
|
93
|
+
son arrêt propre — exactement ce qu'on cherche à éviter.
|
|
94
|
+
- Les deux répondent **avant la limitation de débit** : un kubelet limité recevrait `429` et
|
|
95
|
+
conclurait à un échec.
|
|
96
|
+
|
|
97
|
+
## 4. L'arrêt — ne perdre aucune requête
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
terminationGracePeriodSeconds: 30
|
|
101
|
+
lifecycle: { preStop: { sleep: { seconds: 2 } } }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
La chronologie réelle :
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
SIGTERM ──► /readyz passe à 503 ──► le répartiteur retire le pod ──► drain ≤ 15 s ──► sortie 0
|
|
108
|
+
│ │
|
|
109
|
+
└── preStop : le temps que les points d'accès se propagent ────────────────┘
|
|
110
|
+
fenêtre de grâce : 30 s
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**La fenêtre doit être strictement supérieure au drain** (15 s). À l'expiration, le kubelet tue —
|
|
114
|
+
code **137**, requêtes en vol perdues, aucun message. Le `preStop` court existe parce que le
|
|
115
|
+
retrait d'un point d'accès n'est pas instantané : sans lui, du trafic arrive encore après le début
|
|
116
|
+
du drain.
|
|
117
|
+
|
|
118
|
+
**Vérifier que ça marche** — sous charge constante, `kubectl rollout restart deploy/<app>` : zéro
|
|
119
|
+
`5xx`. C'est la seule preuve qui compte.
|
|
120
|
+
|
|
121
|
+
## 5. Les ressources
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
resources:
|
|
125
|
+
requests: { cpu: 250m, memory: 256Mi }
|
|
126
|
+
limits: { memory: 512Mi } # PAS de limite CPU
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Pas de limite processeur.** Une limite mémoire tue proprement et le pod redémarre ; une limite
|
|
130
|
+
processeur ne tue pas — elle **ralentit en permanence**, et ça ne ressemble pas à une limite, ça
|
|
131
|
+
ressemble à une application lente. Les demandes suffisent à garantir la part.
|
|
132
|
+
|
|
133
|
+
Si tu bornes le tas de la machine virtuelle JavaScript face à une limite mémoire de cgroup,
|
|
134
|
+
dimensionne-le avec une mesure, pas au jugé.
|
|
135
|
+
|
|
136
|
+
## 6. La base de données — elle n'est pas dans le cluster
|
|
137
|
+
|
|
138
|
+
C'est la différence la plus structurante avec le compose de développement.
|
|
139
|
+
|
|
140
|
+
1. **Aucun manifeste de base n'est fourni**, et c'est voulu : en production elle est administrée.
|
|
141
|
+
2. **Ne reporte jamais l'hôte du compose.** `compose.yaml` pointe un **service du compose** ; le
|
|
142
|
+
même nom dans un cluster désigne un Service qui n'existe pas. Rien ne casse au déploiement, tout
|
|
143
|
+
casse au premier accès.
|
|
144
|
+
3. **L'URL passe par un Secret, en `_FILE`** — elle porte un mot de passe, elle n'a rien à faire
|
|
145
|
+
dans un ConfigMap.
|
|
146
|
+
4. **Le plafond de connexions.** Une base administrée les compte strictement. N exemplaires × le
|
|
147
|
+
pool de chacun peuvent la saturer, et l'erreur sort de **son** côté, pas du tien. Reste prudent
|
|
148
|
+
sur le nombre d'exemplaires.
|
|
149
|
+
|
|
150
|
+
| Ta base | Exemplaires | Volume pour `var/` | Stratégie |
|
|
151
|
+
| ---------------------- | ---------------- | ------------------------- | --------------- |
|
|
152
|
+
| SQLite (fichier local) | **1, impératif** | revendication persistante | **`Recreate`** |
|
|
153
|
+
| Serveur (administrée) | plusieurs | `emptyDir` | `RollingUpdate` |
|
|
154
|
+
|
|
155
|
+
> 🔴 **SQLite impose `strategy: Recreate`.** Un déploiement progressif sur un volume monté par un
|
|
156
|
+
> seul nœud attend un disque que l'exemplaire précédent tient encore : le nouveau pod reste en
|
|
157
|
+
> attente **pour toujours**, sans que rien ne nomme la cause.
|
|
158
|
+
|
|
159
|
+
## 7. Les variables du manifeste
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
env:
|
|
163
|
+
- { name: NODE_ENV, value: production }
|
|
164
|
+
- { name: NF_POD_NAME, valueFrom: { fieldRef: { fieldPath: metadata.name } } }
|
|
165
|
+
- { name: NF__HTTP__TRUSTPROXY, value: uniquelocal }
|
|
166
|
+
- { name: NF_CSRF_SECRET_FILE, value: /run/secrets/csrf }
|
|
167
|
+
- { name: NF_DATABASE_URL_FILE, value: /run/secrets/db-url }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`NF_POD_NAME` vient de l'API descendante. Elle dérive l'identité d'origine du bus temps réel : sans
|
|
171
|
+
elle, plusieurs exemplaires se confondent et les messages s'écoutent eux-mêmes.
|
|
172
|
+
|
|
173
|
+
`NF__HTTP__TRUSTPROXY` est **obligatoire** dès qu'un Ingress termine le TLS —
|
|
174
|
+
[`frontal.md`](frontal.md) dit ce qui tombe sans lui, et comment le voir tomber.
|
|
175
|
+
|
|
176
|
+
## 8. Éprouver sans cluster de production
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
kind create cluster --name <app>
|
|
180
|
+
kubectl label ns default pod-security.kubernetes.io/enforce=restricted
|
|
181
|
+
kubectl apply -f deploy/migrate-job.yaml
|
|
182
|
+
kubectl apply -f <ton-deployment>.yaml
|
|
183
|
+
kubectl rollout status deploy/<app>
|
|
184
|
+
kind delete cluster --name <app>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`podman kube play` joue les mêmes fichiers **sans cluster du tout** — mais il ne rejoue ni
|
|
188
|
+
l'admission de la politique, ni l'Ingress, ni un déploiement progressif. Ce qu'il ne prouve pas
|
|
189
|
+
doit être dit, pas supposé prouvé.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Podman — les six écarts avec Docker
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante. Éditer en place.
|
|
4
|
+
|
|
5
|
+
Podman fait tourner cette image. Six choses changent, dont **une qui ne se répare pas** — et c'est
|
|
6
|
+
celle-là qu'il faut connaître avant de mettre une application derrière un frontal.
|
|
7
|
+
|
|
8
|
+
## 1. Le contrôle de santé disparaît
|
|
9
|
+
|
|
10
|
+
Podman construit en format OCI par défaut, et **le format OCI ne porte pas de `HEALTHCHECK`**.
|
|
11
|
+
`podman inspect` rend alors une valeur vide, et toute dépendance qui attend « ce service est sain »
|
|
12
|
+
ne s'arme jamais — donc l'orchestration de ton compose démarre dans le désordre, sans erreur.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
podman build --format docker -t <app> . # le garde
|
|
16
|
+
podman run --health-cmd '…' --health-interval 10s … # ou le poser à l'exécution
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 2. L'arrêt coupe à 10 s — sous le drain
|
|
20
|
+
|
|
21
|
+
`podman stop` attend **10 secondes** par défaut. Le drain de l'application est borné à **15**.
|
|
22
|
+
Chaque arrêt tue donc l'application au milieu de son drain : code **137**, requêtes en vol perdues,
|
|
23
|
+
aucun message.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
podman stop --time 20 <conteneur>
|
|
27
|
+
# ou, dans le compose : stop_grace_period: 20s
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
C'est l'écart le plus fréquent, et celui qui ressemble le moins à un problème de conteneur.
|
|
31
|
+
|
|
32
|
+
## 3. Un montage lié n'est pas inscriptible, en mode sans privilèges
|
|
33
|
+
|
|
34
|
+
En mode sans privilèges, l'identifiant `1000` **du conteneur** est projeté sur un identifiant
|
|
35
|
+
subordonné **de l'hôte** — qui n'est pas ton `1000` à toi. Un dossier de ton disque monté dans le
|
|
36
|
+
conteneur ne lui appartient donc pas : le premier `mkdir` de l'application échoue en `EACCES`.
|
|
37
|
+
|
|
38
|
+
Deux remèdes :
|
|
39
|
+
|
|
40
|
+
- **un volume nommé** — il hérite du porteur du dossier sous-jacent, que l'image a déjà fixé ;
|
|
41
|
+
- **le suffixe `:U`** sur le montage, qui recale le porteur.
|
|
42
|
+
|
|
43
|
+
## 4. Conserver l'identité de l'hôte écrase le `USER` de l'image
|
|
44
|
+
|
|
45
|
+
`--userns=keep-id` **nu** fait tourner le processus sous l'identifiant de l'hôte, et non sous le
|
|
46
|
+
`1000` que l'image déclare. Si ton identifiant d'hôte n'est pas `1000`, les dossiers `/app/tmp` et
|
|
47
|
+
`/app/var` — qui appartiennent à `1000` — lui refusent l'écriture, et le démarrage meurt.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
podman run --userns=keep-id:uid=1000,gid=1000 …
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 5. SELinux refuse les montages liés
|
|
54
|
+
|
|
55
|
+
Sur une distribution avec SELinux en vigueur, un montage lié est refusé (`permission denied`) tant
|
|
56
|
+
qu'il ne porte pas de suffixe de réétiquetage :
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
-v ./docker/db:/docker-entrypoint-initdb.d:ro,z
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Le `z` partage l'étiquette entre conteneurs, le `Z` la rend exclusive. Sur un dossier partagé par
|
|
63
|
+
plusieurs services, c'est `z`.
|
|
64
|
+
|
|
65
|
+
## 6. 🔴 L'adresse du client est perdue — et ça ne se répare pas
|
|
66
|
+
|
|
67
|
+
En mode sans privilèges, la redirection de ports passe par un composant qui **ne préserve pas
|
|
68
|
+
l'adresse source**. Le trafic arrive donc depuis la passerelle, pas depuis le client.
|
|
69
|
+
|
|
70
|
+
Conséquences, toutes **silencieuses** :
|
|
71
|
+
|
|
72
|
+
- l'en-tête d'adresse d'origine porte la passerelle → **l'audit est faux** ;
|
|
73
|
+
- toute limitation par adresse voit **une seule** adresse → elle protège tout le monde de personne,
|
|
74
|
+
ou bloque tout le monde ensemble ;
|
|
75
|
+
- aucune erreur, aucun journal : les valeurs sont plausibles, simplement fausses.
|
|
76
|
+
|
|
77
|
+
**Ce n'est pas un réglage à trouver.** Une autre pile réseau existe, mais son état varie selon les
|
|
78
|
+
versions. La conduite à tenir est de l'**énoncer** — dans le README de ton déploiement, dans le
|
|
79
|
+
ticket, dans la revue — et non de la masquer derrière une configuration qui en aurait l'air.
|
|
80
|
+
|
|
81
|
+
## 7. Ce qui marche pareil
|
|
82
|
+
|
|
83
|
+
| Point | Verdict |
|
|
84
|
+
| --------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
85
|
+
| Un processus d'init dédié | Inutile, comme avec Docker : `node` est le processus n° 1 et reçoit `SIGTERM` directement |
|
|
86
|
+
| Les ports au-dessus de 1024 | `5151`, `8080`, `8443` : rien d'inaccessible en mode sans privilèges |
|
|
87
|
+
| Racine scellée + volumes `tmp` et `var` | Identique |
|
|
88
|
+
| `podman kube play` | Joue tes manifestes **sans cluster** — voir ci-dessous |
|
|
89
|
+
|
|
90
|
+
## 8. `podman kube play` — ce qu'il prouve, et ce qu'il ne prouve pas
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
podman kube play deploy/
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Il prouve** : que le YAML est valide, que l'image démarre avec ce contexte de sécurité, que les
|
|
97
|
+
volumes déclarés suffisent, que les sondes répondent.
|
|
98
|
+
|
|
99
|
+
**Il ne prouve pas** : l'admission de la politique de sécurité (aucun contrôleur d'admission), les
|
|
100
|
+
Ingress, un déploiement progressif, la mise à l'échelle.
|
|
101
|
+
|
|
102
|
+
C'est une preuve locale utile et bon marché. Elle ne remplace pas un cluster jetable — elle évite
|
|
103
|
+
d'en démarrer un pour une faute de frappe.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Secrets, mots de passe, matière cryptographique
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante. Éditer en place.
|
|
4
|
+
|
|
5
|
+
## 1. Le fait qui gouverne tout
|
|
6
|
+
|
|
7
|
+
**Une couche d'image est lisible par quiconque télécharge l'image, et un fichier effacé par une
|
|
8
|
+
couche ultérieure y reste.** Un `COPY` d'un secret suivi d'un `RUN rm` ne supprime rien : le
|
|
9
|
+
secret est toujours dans l'archive, à une commande d'extraction. Un conteneur qui démarre ne le
|
|
10
|
+
montre plus — c'est précisément ce qui rend l'erreur indétectable à l'œil.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx nodefony image:check <app> # lit les COUCHES, pas l'image aplatie. Sort en échec.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
C'est pour ça que ce contrôle existe, et pour ça qu'il ne regarde pas l'image aplatie : un
|
|
17
|
+
contrôle qui la regarderait serait **vert dans le cas exactement le plus fautif**.
|
|
18
|
+
|
|
19
|
+
## 2. Ce qui n'entre jamais dans l'image — le `.dockerignore`
|
|
20
|
+
|
|
21
|
+
Il est rendu avec l'application, et il exclut quatre familles. Ne les retire pas.
|
|
22
|
+
|
|
23
|
+
| Famille | Pourquoi |
|
|
24
|
+
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `**/node_modules`, `**/dist` | Reconstruits **dans** l'image. Entrés depuis ta machine, ils masqueraient la construction et l'image partirait avec le code de la veille. |
|
|
26
|
+
| `var`, `logs`, `**/*.log`, `**/*.jsonl` | Écritures propres à une machine, sans valeur dans une image |
|
|
27
|
+
| `*.local`, `**/*.local` | La convention des fichiers de secrets locaux — jamais commités, et pas davantage dans une image |
|
|
28
|
+
| **La matière cryptographique** | Clés privées, certificats |
|
|
29
|
+
|
|
30
|
+
> ⚠️ **Un motif de `.dockerignore` n'obéit pas aux règles du `.gitignore`.** Sans `**/`, il est
|
|
31
|
+
> **ancré à la racine du contexte** — alors qu'en `.gitignore` il vaut à toute profondeur. Un
|
|
32
|
+
> `*.log` seul ne voit donc aucun journal dans un sous-dossier. Quand tu ajoutes une exclusion,
|
|
33
|
+
> écris `**/motif` sauf si tu veux vraiment ne viser que la racine.
|
|
34
|
+
|
|
35
|
+
## 3. Les certificats — ils se MONTENT, ils ne se gravent pas
|
|
36
|
+
|
|
37
|
+
L'image du frontal ne contient **aucune** clé privée : les certificats sont montés en lecture
|
|
38
|
+
seule à l'exécution. En Kubernetes, un Secret monté en volume ; avec compose, un montage lié.
|
|
39
|
+
|
|
40
|
+
L'étage de construction efface aussi les certificats de développement générés localement, pour
|
|
41
|
+
qu'aucun certificat auto-signé de ta machine ne descende en production.
|
|
42
|
+
|
|
43
|
+
## 4. Les secrets de l'application
|
|
44
|
+
|
|
45
|
+
| Variable | Ce qu'il protège | Doit être partagé entre exemplaires ? |
|
|
46
|
+
| ------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
47
|
+
| `NF_CSRF_SECRET` | Les jetons anti-CSRF | **oui** — sinon un jeton émis par un exemplaire est refusé par l'autre |
|
|
48
|
+
| `NF_TOTP_KEY` | Le chiffrement au repos des secrets de double authentification | **oui** — sinon les seconds facteurs deviennent illisibles |
|
|
49
|
+
| `NF_WEBHOOK_KEY` | Le chiffrement des signatures de webhooks | **oui** |
|
|
50
|
+
| `NF_ADMIN_PASSWORD` | Le compte d'administration | — |
|
|
51
|
+
|
|
52
|
+
> 🔴 **Le partage entre exemplaires n'est pas un détail de confort.** Un secret différent par
|
|
53
|
+
> exemplaire produit des échecs **intermittents** — une requête sur deux, selon le répartiteur.
|
|
54
|
+
> C'est le symptôme le plus coûteux à diagnostiquer, parce qu'il ressemble à un problème réseau.
|
|
55
|
+
|
|
56
|
+
`NF_ADMIN_PASSWORD` a un défaut en développement. **En production il est obligatoire** : sans lui,
|
|
57
|
+
aucun compte d'administration n'est créé et le démarrage échoue — volontairement, plutôt que de
|
|
58
|
+
créer un compte avec un mot de passe connu de tout le monde.
|
|
59
|
+
|
|
60
|
+
## 5. Les monter, pas les poser en clair
|
|
61
|
+
|
|
62
|
+
Le suffixe `_FILE` marche sur **toute** variable — détail complet dans
|
|
63
|
+
[`variables.md`](variables.md). Ce qu'il faut retenir ici :
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
# Kubernetes
|
|
67
|
+
env:
|
|
68
|
+
- name: NF_CSRF_SECRET_FILE
|
|
69
|
+
value: /run/secrets/csrf
|
|
70
|
+
volumeMounts:
|
|
71
|
+
- { name: secrets, mountPath: /run/secrets, readOnly: true }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
# compose
|
|
76
|
+
secrets: [csrf]
|
|
77
|
+
environment:
|
|
78
|
+
NF_CSRF_SECRET_FILE: /run/secrets/csrf
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Pourquoi le fichier plutôt que la variable** : une variable d'environnement est lisible dans
|
|
82
|
+
`/proc/<pid>/environ`, ressort dans `docker inspect`, dans un vidage de processus, et souvent dans
|
|
83
|
+
un rapport d'erreur. Un fichier monté ne ressort d'aucun des quatre.
|
|
84
|
+
|
|
85
|
+
**Poser `KEY` et `KEY_FILE` ensemble fait échouer le démarrage.** L'ambiguïté n'est pas arbitrée en
|
|
86
|
+
silence — c'est voulu.
|
|
87
|
+
|
|
88
|
+
## 6. Rotation
|
|
89
|
+
|
|
90
|
+
Les secrets de session et de CSRF ne peuvent pas changer d'un coup sans invalider ce qui est en
|
|
91
|
+
vol. L'ordre qui limite la casse :
|
|
92
|
+
|
|
93
|
+
1. Poser le nouveau secret **à côté** de l'ancien dans le gestionnaire de secrets.
|
|
94
|
+
2. Redéployer **progressivement** — les exemplaires reprennent le nouveau.
|
|
95
|
+
3. Retirer l'ancien **après** que tous les exemplaires ont repris, pas avant.
|
|
96
|
+
|
|
97
|
+
Entre 2 et 3, les deux valeurs coexistent : c'est la fenêtre où rien ne casse. Sauter l'étape 3
|
|
98
|
+
est l'erreur la plus fréquente — un secret « remplacé » qui traîne encore six mois plus tard.
|
|
99
|
+
|
|
100
|
+
## 7. Ce qu'il ne faut pas faire, et pourquoi c'est tentant
|
|
101
|
+
|
|
102
|
+
| Tentation | Ce qui arrive |
|
|
103
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| Mettre le secret en `ARG` de construction | Les arguments de construction sont **dans les métadonnées de l'image**. `docker history` les montre. |
|
|
105
|
+
| Copier le fichier de secrets puis l'effacer | Il reste dans la couche. Voir §1. |
|
|
106
|
+
| Mettre les secrets dans un ConfigMap | Un ConfigMap n'est ni chiffré au repos ni traité comme sensible par les outils |
|
|
107
|
+
| Laisser le défaut de développement en production | Il est **connu** : il est dans le dépôt public du framework |
|
|
108
|
+
| Passer le secret en ligne de commande | La ligne de commande est visible dans la liste des processus du conteneur |
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Les variables d'environnement
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante. Éditer en place.
|
|
4
|
+
|
|
5
|
+
## 1. La commande qui répond, et à laquelle rien ne ment
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx nodefony env # la cascade des .env, la valeur EFFECTIVE de chaque variable, et sa PROVENANCE
|
|
9
|
+
npx nodefony env --json # le même rapport, pour un script
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
C'est **la** commande d'exploitation. Elle ne dit pas ce que ton manifeste déclare : elle dit ce
|
|
13
|
+
que l'application a **réellement lu**, et d'où ça vient. Toutes les fois où « la variable ne fait
|
|
14
|
+
rien », la réponse est dans cette sortie.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx nodefony inspect config --json # la configuration effective, et la provenance de chaque valeur
|
|
18
|
+
npx nodefony inspect schema <module> # les clés qui EXISTENT, leur type, ce qu'elles font
|
|
19
|
+
npx nodefony doctor --env production # ce qui manquerait là-bas, depuis ici
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> 🔴 **`inspect schema` avant d'écrire une clé.** Une clé inconnue n'est pas refusée : elle est
|
|
23
|
+
> **retirée en silence** à la validation. Tu poses le réglage, rien ne change, rien ne le dit.
|
|
24
|
+
|
|
25
|
+
## 2. Deux grammaires, et elles ne font pas la même chose
|
|
26
|
+
|
|
27
|
+
### `NF_<NOM>` — les variables du catalogue
|
|
28
|
+
|
|
29
|
+
Simple underscore. Ce sont des variables **déclarées**, avec un rôle, un type et un défaut connus.
|
|
30
|
+
|
|
31
|
+
### `NF__<MODULE>__<CHEMIN>` — la surcharge de configuration
|
|
32
|
+
|
|
33
|
+
**Double** underscore. Elle atteint **n'importe quelle clé de configuration de n'importe quel
|
|
34
|
+
module**, sans que cette clé ait eu besoin d'être prévue comme variable. Le séparateur en double
|
|
35
|
+
underscore est la convention .NET Core et Docker : explicite, et sans ambiguïté avec le camelCase.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
NF__SECURITY__JWT__ACCESSTTLS=300
|
|
39
|
+
NF__HTTP__SERVERS__HTTPS__PORT=8443
|
|
40
|
+
NF__SECURITY__CORS__ORIGINS=https://a.com,https://b.com # la virgule fait un tableau
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- Le **premier segment est le module** (`SECURITY` → `@nodefony/security`), les suivants sont le
|
|
44
|
+
chemin dans sa configuration.
|
|
45
|
+
- Les segments sont **insensibles à la casse** et résolus contre les clés réelles :
|
|
46
|
+
`ACCESSTTLS` trouve `accessTtlS`.
|
|
47
|
+
- **Précédence** : appliquée **après** la configuration de l'application, et **avant** la
|
|
48
|
+
validation du schéma. Donc une valeur surchargée est **validée comme les autres** — une valeur
|
|
49
|
+
aberrante fait échouer le démarrage au lieu de s'installer.
|
|
50
|
+
- Résolue **une fois au démarrage**. Aucun coût par requête.
|
|
51
|
+
|
|
52
|
+
**La conversion est explicite**, pas devinée par une bibliothèque : `true`/`false` deviennent un
|
|
53
|
+
booléen, un nombre devient un nombre, `[…]` et `{…}` sont lus en JSON, une chaîne à virgules
|
|
54
|
+
devient un tableau, le reste reste une chaîne. C'est ce qui évite le piège classique où la chaîne
|
|
55
|
+
`"false"` est convertie en booléen **vrai**.
|
|
56
|
+
|
|
57
|
+
## 3. Le suffixe `_FILE` — les secrets montés
|
|
58
|
+
|
|
59
|
+
**Toute** variable du catalogue accepte un `_FILE`. Le contenu du fichier est lu, retour à la ligne
|
|
60
|
+
final retiré.
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
env:
|
|
64
|
+
- name: NF_CSRF_SECRET_FILE
|
|
65
|
+
value: /run/secrets/csrf
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Trois comportements à connaître, et les deux derniers sont ce qui distingue ce mécanisme d'un
|
|
69
|
+
mécanisme complaisant :
|
|
70
|
+
|
|
71
|
+
| Situation | Ce qui se passe |
|
|
72
|
+
| ---------------------------------------------- | ------------------------------------------------------------------- |
|
|
73
|
+
| `KEY` absente, `KEY_FILE` pointe un fichier | La valeur est lue dans le fichier |
|
|
74
|
+
| **`KEY` ET `KEY_FILE` posées toutes les deux** | **Le démarrage ÉCHOUE** — l'ambiguïté n'est pas arbitrée en silence |
|
|
75
|
+
| `KEY_FILE` pointe un fichier **illisible** | **Le démarrage ÉCHOUE** — pas de repli muet sur un défaut |
|
|
76
|
+
|
|
77
|
+
C'est délibéré : un secret qu'on croit monté et qui ne l'est pas est une application qui tourne
|
|
78
|
+
avec un secret de développement, en production, sans que personne le sache.
|
|
79
|
+
|
|
80
|
+
Marche avec les secrets Docker, les secrets Kubernetes, et tout agent qui dépose un fichier.
|
|
81
|
+
|
|
82
|
+
## 4. Les variables de l'exploitation
|
|
83
|
+
|
|
84
|
+
Les seules qui te concernent au déploiement. Le catalogue complet, c'est `npx nodefony env`.
|
|
85
|
+
|
|
86
|
+
### Environnement et réseau
|
|
87
|
+
|
|
88
|
+
| Variable | Rôle | Absente ⇒ |
|
|
89
|
+
| --------------- | -------------------------- | -------------------- |
|
|
90
|
+
| `NF_ENV` | L'environnement applicatif | Dérivé de `NODE_ENV` |
|
|
91
|
+
| `NF_PORT` | Port HTTP | `5151` |
|
|
92
|
+
| `NF_PORT_HTTPS` | Port HTTPS | `5152` |
|
|
93
|
+
|
|
94
|
+
### Derrière un frontal — **les plus importantes**
|
|
95
|
+
|
|
96
|
+
| Variable | Rôle | Absente ⇒ |
|
|
97
|
+
| ---------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
98
|
+
| `NF__HTTP__TRUSTPROXY` | Quels intermédiaires sont crus sur leurs en-têtes | **Le schéma constaté est `http`** → cookie `__Host-` non émis, adresse client fausse |
|
|
99
|
+
| `NF__HTTP__TRUSTEDHOSTS` | Les hôtes servis | Un `Host` étranger n'est pas refusé |
|
|
100
|
+
| `NF__APP__DOMAINCHECK` | Contrôle du domaine | Pas de contrôle |
|
|
101
|
+
| `NF__HTTP__STATICS__ENABLED` | L'application sert-elle les fichiers statiques | Elle les sert — donc en double avec ton frontal |
|
|
102
|
+
|
|
103
|
+
### Données
|
|
104
|
+
|
|
105
|
+
| Variable | Rôle | Absente ⇒ |
|
|
106
|
+
| ----------------- | ---------------------- | ---------------------------------------------------- |
|
|
107
|
+
| `NF_DATABASE_URL` | L'URL de la base | **Repli sur SQLite local**, dans `var/` — silencieux |
|
|
108
|
+
| `NF_REDIS_URL` | L'URL du cache partagé | Le cache n'est pas chargé |
|
|
109
|
+
|
|
110
|
+
> Le repli SQLite est le piège d'exploitation classique : l'application **démarre**, sert des
|
|
111
|
+
> requêtes, écrit dans un fichier — et chaque exemplaire a sa propre base. Rien n'est en erreur.
|
|
112
|
+
> `npx nodefony env` le montre en une ligne.
|
|
113
|
+
|
|
114
|
+
### Secrets — obligatoires en production
|
|
115
|
+
|
|
116
|
+
| Variable | Ce qu'il protège |
|
|
117
|
+
| ------------------- | ------------------------------------------------ |
|
|
118
|
+
| `NF_CSRF_SECRET` | Les jetons anti-CSRF, partagés entre exemplaires |
|
|
119
|
+
| `NF_TOTP_KEY` | Les secrets de double authentification au repos |
|
|
120
|
+
| `NF_WEBHOOK_KEY` | Les signatures de webhooks |
|
|
121
|
+
| `NF_ADMIN_PASSWORD` | Le compte d'administration |
|
|
122
|
+
|
|
123
|
+
Tous en `_FILE` en production. Détail et rotation → [`secrets.md`](secrets.md).
|
|
124
|
+
|
|
125
|
+
### Plusieurs exemplaires
|
|
126
|
+
|
|
127
|
+
| Variable | Rôle |
|
|
128
|
+
| --------------------------------- | ---------------------------------------------------------------------------- |
|
|
129
|
+
| `NF_POD_NAME` | Identité de l'exemplaire — **dérive l'identité d'origine du bus temps réel** |
|
|
130
|
+
| `NF_INSTANCE_ID` | Identifiant d'exemplaire |
|
|
131
|
+
| `NF_REALTIME_DRIVER` | Le transport du bus entre exemplaires |
|
|
132
|
+
| `NF_REALTIME_BACKPLANE_SECRET` | Scelle les enveloppes échangées sur le bus |
|
|
133
|
+
| `NF_REALTIME_BACKPLANE_NAMESPACE` | Cloisonne deux applications qui partagent le même bus |
|
|
134
|
+
|
|
135
|
+
> 🔴 **Deux applications sur un même cache, sans espace de noms distinct, se parlent.** Le symptôme
|
|
136
|
+
> est un message qui arrive à des clients d'une autre application — jamais une erreur.
|
|
137
|
+
|
|
138
|
+
### Multi-process sur une seule machine
|
|
139
|
+
|
|
140
|
+
| Variable | Rôle |
|
|
141
|
+
| ------------ | -------------------------------------------------------- |
|
|
142
|
+
| `NF_CLUSTER` | Active le mode multi-process |
|
|
143
|
+
| `NF_WORKERS` | Nombre d'exécutants — sinon dérivé des limites du cgroup |
|
|
144
|
+
|
|
145
|
+
En Kubernetes, **laisse ça tranquille** : un exemplaire = un processus = un pod, et c'est
|
|
146
|
+
l'ordonnanceur qui multiplie. Le mode multi-process sert sur une machine nue.
|
|
147
|
+
|
|
148
|
+
### Démarrage et journalisation
|
|
149
|
+
|
|
150
|
+
| Variable | Rôle |
|
|
151
|
+
| -------------------- | ------------------------------------------------------- |
|
|
152
|
+
| `NF_BOOT_TIMEOUT_MS` | Au-delà, le démarrage abandonne — borne ta sonde dessus |
|
|
153
|
+
| `NF_BOOT_WARN_MS` | Au-delà, il avertit |
|
|
154
|
+
| `NF_LOG_DRIVER` | `stdout` (défaut), `file`, ou aucun |
|
|
155
|
+
| `NF__DEBUG` | Débogage ciblé, par module |
|
|
156
|
+
|
|
157
|
+
En conteneur, **laisse `NF_LOG_DRIVER` sur `stdout`** : c'est ce que ton collecteur ramasse. Écrire
|
|
158
|
+
dans un fichier à l'intérieur d'un conteneur, c'est écrire dans quelque chose qui disparaît.
|
|
159
|
+
|
|
160
|
+
## 5. Le préfixe `NF_` — pourquoi, et ce que ça t'évite
|
|
161
|
+
|
|
162
|
+
**Tout ce que Nodefony lit est préfixé `NF_`.** Pas par coquetterie : ton environnement de
|
|
163
|
+
production a déjà des `REDIS_HOST`, des `COOKIE_SECRET`, des `POD_NAME` que d'autres outils
|
|
164
|
+
revendiquent. Une collision ne produit **jamais** une erreur — elle produit un comportement
|
|
165
|
+
inexplicable.
|
|
166
|
+
|
|
167
|
+
Deux exceptions, et deux seulement :
|
|
168
|
+
|
|
169
|
+
- Ce que Nodefony ne possède pas : `NODE_ENV`, `CI`, `NODE_DEBUG`… — lues telles quelles.
|
|
170
|
+
- Les alias qu'un hébergeur **pose lui-même** (`DATABASE_URL`, `REDIS_URL`, `APP_ENV`) : acceptés,
|
|
171
|
+
mais **en second rang** derrière la forme `NF_`.
|
|
172
|
+
|
|
173
|
+
> ⚠️ Corollaire d'exploitation : si tu poses `REDIS_URL` **et** `NF_REDIS_URL`, c'est la forme
|
|
174
|
+
> préfixée qui gagne. Purger l'une en croyant avoir purgé l'autre est une erreur déjà vue.
|
|
@@ -9,20 +9,26 @@ description: >
|
|
|
9
9
|
exemplaires. À charger AVANT de modifier une entité déjà en base, ou avant de déployer un schéma
|
|
10
10
|
changé.
|
|
11
11
|
Déclencheurs : "j'ai ajouté un champ à une entité", "la colonne n'existe pas en base",
|
|
12
|
-
"modifier une table existante", "
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"comment tester ma migration", "éprouver une migration", "vérifier qu'une migration marche",
|
|
18
|
-
"prouver que ma migration s'applique", "essayer sans casser ma base", "base d'essai",
|
|
19
|
-
"rejouer les migrations depuis zéro", "repartir d'une base propre".
|
|
12
|
+
"modifier une table existante", "migrer le schéma", "orm:migrate", "orm:generate",
|
|
13
|
+
"appliquer les migrations", "déployer un changement de schéma", "no such column",
|
|
14
|
+
"column does not exist", "ma base ne correspond plus au code", "adopter une base existante",
|
|
15
|
+
"réparer une migration en échec", "éprouver une migration sans casser ma base",
|
|
16
|
+
"rejouer les migrations depuis zéro".
|
|
20
17
|
metadata:
|
|
21
18
|
version: 2
|
|
22
19
|
---
|
|
23
20
|
|
|
24
21
|
# Faire évoluer un schéma, et le porter en production
|
|
25
22
|
|
|
23
|
+
> 🧭 **Tu es arrivé ici directement ? Charge aussi `nodefony-dev`** — il porte la conduite
|
|
24
|
+
> commune (par où commencer, comment prouver que c'est fait) et les pièges qui coûtent une heure,
|
|
25
|
+
> serveur comme front. Cette page-ci ne couvre QUE son geste.
|
|
26
|
+
>
|
|
27
|
+
> Et si une réponse te manque, elle est probablement INSTALLÉE : `rg` ne descend pas dans
|
|
28
|
+
> `node_modules`, donc 70 pages de documentation y paraissent absentes. Une commande les lit, avec
|
|
29
|
+
> la ligne exacte :
|
|
30
|
+
> `node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs <termes>`.
|
|
31
|
+
|
|
26
32
|
## 1. La seule chose à savoir avant tout le reste
|
|
27
33
|
|
|
28
34
|
**En développement, il n'y a rien à faire.** La base suit le code : la table naît au démarrage, et
|
|
@@ -139,15 +145,21 @@ npx nodefony orm:migrate
|
|
|
139
145
|
d'appliquer ». Il ne refuse pas — il ne lit pas la base et ignore si ta table porte des lignes —,
|
|
140
146
|
mais s'il le signale et que ta table n'est pas vide, la migration échouera.
|
|
141
147
|
|
|
142
|
-
**À l'étape 3, sur sqlite, attends-toi à un
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
**À l'étape 3, sur sqlite, attends-toi à un AVERTISSEMENT `table-rebuild`, pas à un refus.**
|
|
149
|
+
Rendre une colonne obligatoire n'est pas un `ALTER` en sqlite : le moteur n'en a pas, alors la
|
|
150
|
+
table est RECONSTRUITE (`CREATE` une table d'étape → `INSERT … SELECT` → `DROP TABLE` →
|
|
151
|
+
`RENAME`). Ce `DROP TABLE` porte sur une table recopiée une ligne plus haut, dans la même
|
|
152
|
+
migration : l'outil le reconnaît et te le signale — « recrée la table pour modifier une colonne
|
|
153
|
+
(SQLite ne sait pas faire autrement) » — sans rien refuser, à la génération comme à la mise en
|
|
154
|
+
service. Tu n'as donc **pas** besoin de `--allow-destructive` pour une reconstruction.
|
|
155
|
+
|
|
156
|
+
Ce qui reste à TA charge, et c'est le sens de l'avertissement : **relis l'`INSERT … SELECT`**.
|
|
157
|
+
Une colonne absente de cette recopie serait bel et bien perdue, et aucun automate ne peut le
|
|
158
|
+
deviner à ta place. Éprouvé de bout en bout : deux lignes semées, trois étapes, deux lignes
|
|
159
|
+
intactes et l'index unique en place.
|
|
160
|
+
|
|
161
|
+
> Un vrai `DROP TABLE` — une table supprimée sans recopie, ou une AUTRE table supprimée dans la
|
|
162
|
+
> même migration — reste refusé. Le verdict porte sur chaque table, pas sur le fichier.
|
|
151
163
|
|
|
152
164
|
> **Ne jamais** répondre à un échec de migration en refaisant la base. Une migration qui n'est pas
|
|
153
165
|
> passée n'a **rien** changé — sqlite et PostgreSQL l'annulent entière. C'est le fichier qu'il faut
|