Docker et Kubernetes
Un processus, un volume, un fichier à sauvegarder. La plus longue section de cette page est celle qui dit quoi poser avant que quelqu’un d’autre que vous puisse l’atteindre.
Un conteneur
Aucune version publiée pour l'instant.
L'image, le chart et les paquets que nomment les commandes de ce site sont la distribution prévue. Aucun n'est publié. Ce qui fonctionne aujourd'hui est une compilation depuis les sources, qui demande Rust et Node :
Compiler et lancer
git clone https://github.com/mcpd/mcpd && cd mcpd && make build && ./target/release/mcpd demo mcpd est un processus unique dont le stockage est compilé dedans. Il n’y a pas de base à faire tourner à côté, pas de side-car et pas de serveur de fichiers statiques : l’ingestion, l’API de requête et l’interface sont toutes servies depuis un seul port.
docker run -p 4317:4317 -v mcpd-data:/data -e MCPD_DATA=/data/mcpd.db ghcr.io/mcpd/mcpd:0.1.0
Le volume est la partie qui mérite deux lectures. Sans lui, le fichier
SQLite vit dans la couche inscriptible du conteneur, et les traces partent
avec le conteneur. MCPD_DATA est posée explicitement pour la
même raison : si le chemin qu’écrit le daemon et le chemin que vous avez
monté viennent à diverger, mcpd continue de marcher et le volume reste vide,
ce qui est le genre d’erreur que l’on découvre le jour où l’on a besoin des
données.
Figez l’étiquette. latest transforme un redémarrage en mise à
niveau, à un moment que personne n’a choisi. Chaque étiquette est construite
pour linux/amd64 et linux/arm64, et tourne sous
l’uid 10001, propriétaire de /data dans l’image.
Le port, et ce qui répond dessus
Tout est sur 4317. C’est ce qui fait tenir le déploiement en un conteneur, et c’est aussi ce qu’il vous faut savoir pour écrire une règle d’ingress ou une politique réseau, parce que les chemins ne portent pas tous le même risque.
| Chemin | Ce que c’est | Clé requise |
|---|---|---|
/v1/traces, /v1/logs, /v1/metrics | Ingestion OTLP, pour n’importe quel SDK OpenTelemetry | Ingestion |
/v1/ingest | Ingestion native, le modèle complet en une requête | Ingestion |
/v1/api/… | L’API de requête : traces, spans, logs, scores, analyses | Lecture |
/ | L’interface d’exploitation, embarquée dans le binaire | Aucune |
/healthz, /readyz, /metrics | Vivacité, disponibilité, et les compteurs Prometheus de mcpd | Aucune |
Une requête d’ingestion est plafonnée à 32 MiB. Les prompts sont gros, donc cette limite est large à dessein, mais un proxy placé devant mcpd en a d’habitude une plus petite : un proxy inverse qui refuse un lot de 3 MiB est la raison la plus courante pour qu’une trace n’arrive jamais.
Compose
Deux décisions de ce fichier méritent une explication, parce que les deux
ressemblent à des oublis. Le port est publié sur 127.0.0.1 :
rien hors de l’hôte ne peut donc l’atteindre pendant que vous posez encore
les clés. Et le fichier n’a pas de healthcheck parce que l’image en a un :
elle interroge /healthz toutes les 30 secondes avec le
wget de busybox qu’elle contient, sur le port de
MCPD_LISTEN, et docker compose ps en affiche le
résultat. Docker ne redémarre pas un conteneur parce qu’il est en mauvaise
santé ; la politique de redémarrage agit quand le processus sort.
deploy/docker
# mcpd: one service, one volume.
#
# The volume is most of the reason this file exists. Without it the SQLite file
# lives in the container's writable layer, and `docker compose down` takes every
# trace with it.
#
# Copy .env.example to .env and fill it in before running this. The keys are not
# in this file on purpose: a compose file is committed, a .env file is not.
services:
mcpd:
image: ghcr.io/mcpd/mcpd:0.1.0
restart: unless-stopped
# Published on the loopback interface of the host, so nothing outside this
# machine can reach the port even while you are still setting up keys.
ports:
- "127.0.0.1:4317:4317"
environment:
MCPD_LISTEN: 0.0.0.0:4317
# Set the path explicitly. If this and the mount below ever disagree, the
# daemon writes to the container layer and the volume stays empty.
MCPD_DATA: /data/mcpd.db
MCPD_PROJECT: default
MCPD_RETENTION_DAYS: "30"
MCPD_LOG_FORMAT: json
MCPD_INGEST_KEYS: ${MCPD_INGEST_KEYS:?set MCPD_INGEST_KEYS in .env}
MCPD_READ_KEYS: ${MCPD_READ_KEYS:?set MCPD_READ_KEYS in .env}
MCPD_REDACT_KEY: ${MCPD_REDACT_KEY:?set MCPD_REDACT_KEY in .env}
# The run store: a second file on the same volume.
MCPD_RUNS_DATA: /data/mcpd.runs.db
# The two scopes of the run surface. Unset accepts the write keys, which
# here are the ingest keys; set them in .env once a worker runs outside
# this host. No value here on purpose: Compose passes the variable only
# when .env sets it, and `${VAR:-}` would pass an empty string, which the
# daemon reads as one empty key rather than none.
MCPD_EXECUTE_KEYS:
MCPD_WORKER_KEYS:
MCPD_REQUEST_TIMEOUT_SECONDS: "30"
# The two bounds on one request: bytes of body, and signals in it once
# decoded. Lower the first on a small host, since it is what decides how
# much memory one request can take before anything is parsed.
MCPD_MAX_BODY_BYTES: "33554432"
MCPD_MAX_BATCH_SIGNALS: "10000"
# Set MCPD_CORS_ORIGINS when the interface is served from another origin,
# MCPD_REDACT_KEY_CONTAINS to mask fields the built-in rules do not know,
# and MCPD_REDACT_MAX_STRING to lower the 128 KiB cap on a stored string.
# MCPD_SIGNUP=true opens account sign-up; the accounts go to
# MCPD_ACCOUNTS_DATA, beside the data file by default. Behind a proxy, set
# MCPD_CLIENT_IP_HEADER and MCPD_TRUSTED_PROXY_CIDRS so the sign-up and
# log-in limits count clients rather than the proxy.
volumes:
- mcpd-data:/data
# No healthcheck here, because the image has one: it requests /healthz
# every 30 seconds with the busybox wget in the image, on the port in
# MCPD_LISTEN, and `docker compose ps` shows the result. It is the liveness
# endpoint, never /readyz, so a slow query does not mark the daemon sick.
# Docker does not restart a container for being unhealthy; the restart
# policy above acts when the process exits.
# Longer than the 30-second request timeout above, so a request still
# running at `docker compose down` can finish before Docker kills it.
stop_grace_period: 45s
volumes:
mcpd-data: # Copy to .env and replace every value. Generate each one with:
# openssl rand -hex 32
# Keys permitted to write. Comma-separated, one per fleet or per team, so a
# leaked key can be revoked without re-deploying every agent.
MCPD_INGEST_KEYS=replace-me-ingest
# Keys permitted to read stored prompts and completions. Never the same value
# as an ingest key.
MCPD_READ_KEYS=replace-me-read
# Salt for the hashes redaction leaves behind. Without it the digests of short
# identifiers can be recovered by guessing.
MCPD_REDACT_KEY=replace-me-redact
# Keys for the run surface. Leave both commented out and the ingest keys are
# accepted for starting runs and claiming steps, which is fine while every
# worker runs on this host. Set them once a worker runs elsewhere: the worker
# key lives in the process that runs step bodies and opens nothing that reads a
# prompt. Uncomment a line only with a value: `MCPD_WORKER_KEYS=` with nothing
# after it passes an empty string, not an unset variable.
# MCPD_EXECUTE_KEYS=replace-me-execute
# MCPD_WORKER_KEYS=replace-me-worker docker compose -f deploy/docker/docker-compose.yml up -d
Le :? de chaque ligne de clé n’est pas décoratif. Compose refuse
de démarrer le service quand la variable n’est pas définie, ce qui change une
clé oubliée d’un port ouvert silencieux en un message d’erreur. Les deux clés
des exécutions n’ont aucune valeur dans le fichier : Compose ne les transmet
que si .env les définit, car une valeur vide arriverait au démon
comme une clé vide, et non comme aucune clé.
Kubernetes
Quatre objets : un Deployment, un Service, un PersistentVolumeClaim, et un
Secret que vous créez vous-même. Le Deployment fait tourner une réplique avec
la stratégie Recreate, et c’est la seule ligne de ces manifestes
que vous ne devriez pas changer. mcpd stocke tout dans un seul fichier
SQLite, un seul processus l’écrit, et une mise à jour progressive démarrerait
un second pod sur le même volume pendant que le premier le tient encore. Ce
n’est pas une histoire de mise à l’échelle, c’est une histoire de corruption.
Le Secret ne fait pas partie des fichiers que kubectl apply lit.
secret.yaml.example en montre la forme, et son extension le
tient hors de l’apply : un Secret appliqué tel quel ferait de ses chaînes de
remplacement vos clés. Créez-le plutôt en ligne de commande, avec des clés
générées sur place. Le pod tourne sous l’uid 10001, avec un système de
fichiers racine en lecture seule, aucune capability et le profil seccomp par
défaut du runtime, ce que demande le niveau Pod Security
restricted.
deploy/k8s
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcpd
labels:
app.kubernetes.io/name: mcpd
spec:
# One SQLite file has one writer. Two pods mounting the same volume is not a
# scaling story, it is a corruption story.
replicas: 1
strategy:
# RollingUpdate would start the new pod while the old one still holds the
# database. Recreate stops the old pod first and costs a few seconds of
# downtime, which the SDK buffers cover.
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: mcpd
template:
metadata:
labels:
app.kubernetes.io/name: mcpd
spec:
securityContext:
runAsNonRoot: true
# The uid and gid the image creates its user with, so this pod and a
# plain `docker run` own files the same way.
runAsUser: 10001
runAsGroup: 10001
# The volume is created owned by root. Without fsGroup the daemon
# cannot create its database and exits on the first start.
fsGroup: 10001
# The Pod Security `restricted` level refuses a pod without it.
seccompProfile:
type: RuntimeDefault
# Longer than the 30-second request timeout below, so a request still
# running at SIGTERM can finish before the kubelet kills the process.
terminationGracePeriodSeconds: 45
containers:
- name: mcpd
image: ghcr.io/mcpd/mcpd:0.1.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 4317
env:
- name: MCPD_LISTEN
value: 0.0.0.0:4317
- name: MCPD_DATA
value: /data/mcpd.db
- name: MCPD_PROJECT
value: default
- name: MCPD_RETENTION_DAYS
value: "30"
- name: MCPD_LOG_FORMAT
value: json
- name: MCPD_INGEST_KEYS
valueFrom:
secretKeyRef:
name: mcpd-keys
key: ingest-keys
- name: MCPD_READ_KEYS
valueFrom:
secretKeyRef:
name: mcpd-keys
key: read-keys
- name: MCPD_ADMIN_KEYS
valueFrom:
secretKeyRef:
name: mcpd-keys
key: admin-keys
# The two scopes of the run surface. A worker key lives in the
# process that runs step bodies and opens nothing that reads a
# stored prompt. Optional: with no entry in the Secret the
# variable is not set at all, and the ingest keys are accepted.
- name: MCPD_EXECUTE_KEYS
valueFrom:
secretKeyRef:
name: mcpd-keys
key: execute-keys
optional: true
- name: MCPD_WORKER_KEYS
valueFrom:
secretKeyRef:
name: mcpd-keys
key: worker-keys
optional: true
# The run store is a second file on the same volume.
- name: MCPD_RUNS_DATA
value: /data/mcpd.runs.db
- name: MCPD_REQUEST_TIMEOUT_SECONDS
value: "30"
# The two bounds on one request: bytes of body, and signals in it
# once decoded. Lower the first on a small pod, since it is what
# decides how much memory one request can take.
- name: MCPD_MAX_BODY_BYTES
value: "33554432"
- name: MCPD_MAX_BATCH_SIGNALS
value: "10000"
# Set when the interface is served from another origin, as a
# comma-separated list of origins; and to mask fields the built-in
# rules do not know, or to lower the 128 KiB cap on a stored string:
# MCPD_CORS_ORIGINS, MCPD_REDACT_KEY_CONTAINS, MCPD_REDACT_MAX_STRING
- name: MCPD_REDACT_KEY
valueFrom:
secretKeyRef:
name: mcpd-keys
key: redact-key
# Liveness never touches storage, so a slow query cannot restart a
# process that is working.
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
# Readiness pings the store, so traffic only reaches an instance that
# can actually write to its database.
readinessProbe:
httpGet:
path: /readyz
port: http
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
# Migrations run at start. On a large database that takes longer than
# the liveness probe would allow, so the startup probe holds it off.
startupProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 2
failureThreshold: 45
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
# No CPU limit: ingest arrives in bursts, and throttling the
# writer is how a queue turns into dropped spans.
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: mcpd-data apiVersion: v1
kind: Service
metadata:
name: mcpd
labels:
app.kubernetes.io/name: mcpd
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: mcpd
ports:
- name: http
port: 4317
targetPort: http
protocol: TCP apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mcpd-data
labels:
app.kubernetes.io/name: mcpd
spec:
accessModes:
# ReadWriteOnce, and not because of a quota. SQLite locking is only correct
# on a local block device: on NFS or any other shared filesystem the locks
# are advisory at best and the database will eventually be corrupted.
- ReadWriteOnce
resources:
requests:
storage: 20Gi
# storageClassName: gp3 # A template, not part of the files beside it.
#
# The extension is .example on purpose. `kubectl apply -f deploy/k8s/` applies
# every manifest in this directory, and a Secret applied as written would put
# these placeholder strings into production as the keys. Create the Secret
# from the command line instead, with keys generated on the spot:
#
# kubectl -n mcpd create secret generic mcpd-keys \
# --from-literal=ingest-keys="$(openssl rand -hex 32)" \
# --from-literal=read-keys="$(openssl rand -hex 32)" \
# --from-literal=admin-keys="$(openssl rand -hex 32)" \
# --from-literal=redact-key="$(openssl rand -hex 32)"
#
# Add execute-keys and worker-keys the same way once a worker runs outside the
# cluster; until then the deployment accepts the ingest keys for both.
#
# Or copy this file, replace every value with the output of
# `openssl rand -hex 32`, apply it by name, and do not commit the result.
apiVersion: v1
kind: Secret
metadata:
name: mcpd-keys
labels:
app.kubernetes.io/name: mcpd
type: Opaque
stringData:
# Comma-separated. One key per fleet or per team means a leaked key can be
# revoked without redeploying every agent.
ingest-keys: REPLACE_WITH_A_RANDOM_KEY
# Never the same value as an ingest key.
read-keys: REPLACE_WITH_A_DIFFERENT_RANDOM_KEY
# Keys permitted to delete data and read the audit log. Without this entry
# the daemon accepts the read keys for admin work, so every read key can
# delete a trace; `mcpd doctor` reports that as a warning.
admin-keys: REPLACE_WITH_A_THIRD_RANDOM_KEY
# Keys permitted to start, cancel and signal runs.
execute-keys: REPLACE_WITH_A_FIFTH_RANDOM_KEY
# Keys permitted to claim steps and report their outcome. This one lives in
# the process that runs step bodies, so it is never the read key.
worker-keys: REPLACE_WITH_A_SIXTH_RANDOM_KEY
# Salt for the hashes redaction leaves behind.
redact-key: REPLACE_WITH_A_FOURTH_RANDOM_KEY export INGEST_KEY=$(openssl rand -hex 32) READ_KEY=$(openssl rand -hex 32) ADMIN_KEY=$(openssl rand -hex 32)
kubectl create namespace mcpd
kubectl -n mcpd create secret generic mcpd-keys \
--from-literal=ingest-keys=$INGEST_KEY \
--from-literal=read-keys=$READ_KEY \
--from-literal=admin-keys=$ADMIN_KEY \
--from-literal=redact-key=$(openssl rand -hex 32)
kubectl -n mcpd apply -f deploy/k8s/
Gardez les trois clés là où votre équipe garde ses secrets : la clé
d’ingestion va à chaque agent, la clé de lecture à qui lit l’interface, et la
clé d’administration à qui peut supprimer des données. Ajoutez
execute-keys et worker-keys de la même façon dès
qu’un worker tourne hors du cluster ; d’ici là, les clés d’ingestion sont
acceptées pour les deux.
export EXECUTE_KEY=$(openssl rand -hex 32) WORKER_KEY=$(openssl rand -hex 32)
kubectl -n mcpd patch secret mcpd-keys --type merge \
-p '{"stringData":{"execute-keys":"'$EXECUTE_KEY'","worker-keys":"'$WORKER_KEY'"}}'
kubectl -n mcpd rollout restart deploy/mcpd kubectl -n mcpd port-forward svc/mcpd 4317:4317 Les agents à l’intérieur du cluster pointent sur le Service. Les SDK lisent ces deux valeurs dans l’environnement : du code déjà instrumenté n’a donc rien à changer.
MCPD_ENDPOINT=http://mcpd.mcpd.svc:4317 MCPD_API_KEY=$INGEST_KEY Les sondes
Les deux endpoints font volontairement des choses différentes, et les intervertir cause une panne précise et laide.
-
/healthzne touche jamais au stockage. Il répond depuis le seul état du processus. Une sonde de vivacité qui interrogerait la base redémarrerait un daemon en bonne santé chaque fois qu’une grosse requête le ralentit, et redémarrer est exactement ce que vous ne voulez pas pendant qu’un journal d’écriture anticipée est en train d’être vidé. -
/readyzping le stockage et rend un 503 avec"status": "degraded"et une ligne disant ce qui ne va pas quand il n’y arrive pas. Cela tient le trafic à l’écart d’une instance qui ne peut pas le servir, sans la tuer.
La sonde de démarrage est là parce que les migrations tournent au lancement. Elles sont idempotentes et en général instantanées, mais sur une grosse base elles peuvent prendre plus de temps que la sonde de vivacité ne tolérerait, et un pod tué au milieu d’une migration à chaque tentative ne démarre jamais du tout.
Ce qu’il faut lui donner
Ces nombres viennent de ce que le processus fait réellement, pas d’un banc d’essai, et aucun banc d’essai n’est revendiqué.
| Ressource | Valeur | Pourquoi |
|---|---|---|
| Demande de CPU | 100m | Au repos, cela ne coûte presque rien. La demande existe pour que l’ordonnanceur ne l’empile pas sous quelque chose de bruyant. |
| Limite de CPU | aucune | L’ingestion arrive en rafales. Brider l’écrivain pendant une rafale est la façon dont une file cliente se remplit et dont des spans se perdent. |
| Demande de mémoire | 128Mi | Un binaire statique, cinq connexions SQLite et leurs caches de pages. |
| Limite de mémoire | 512Mi | De la marge pour l’ingestion concurrente. Une requête peut tenir 32 MiB de texte de prompt, donc la mémoire suit le nombre de gros lots qui arrivent en même temps. |
| Volume | 20Gi, ReadWriteOnce | Dimensionné depuis vos propres payloads. Le texte domine, et l’index de recherche n’est pas un index à contenu externe : les prompts et les complétions indexés sont donc stockés une seconde fois. |
Gardez le volume sur un périphérique bloc local. Le verrouillage de SQLite n’est correct que là : sur NFS ou un autre système de fichiers partagé, les verrous sont consultatifs au mieux, et la panne ne se montre pas comme une erreur, elle se montre comme une base corrompue des semaines plus tard.
Le chart Helm
Le chart rend les mêmes quatre objets avec des valeurs devant eux, plus cinq qui restent inactifs jusqu’à ce qu’une valeur les active : un Ingress, une NetworkPolicy, un PodDisruptionBudget, un ServiceAccount et un ServiceMonitor. Générez d’abord les clés et gardez-les là où votre équipe garde ses secrets, parce que l’installation ne vous les réaffichera pas :
helm install mcpd oci://ghcr.io/mcpd/charts/mcpd -n mcpd --create-namespace --set keys.ingest=$INGEST_KEY --set keys.read=$READ_KEY
L’installation échoue quand keys.ingest ou
keys.read n’est pas définie, et elle dit pourquoi. Un chart qui
vous laisserait découvrir plus tard un endpoint d’ingestion ouvert ne ferait
pas son travail. Si vous gérez le Secret vous-même, posez plutôt
keys.existingSecret et le chart n’en crée aucun.
Le gabarit de pod porte une somme de contrôle du Secret : changer une clé
puis lancer helm upgrade redémarre donc le pod. Sans cela, la
nouvelle clé reste dans le cluster et le daemon continue d’appliquer
l’ancienne.
Les moniteurs, les SLO, le juge, les clés par projet et les délais du
magasin d’exécutions se lisent dans le fichier de configuration du daemon,
pas dans des indicateurs. Écrivez cet objet JSON sous
configFile.content : le chart le monte en lecture seule et y
fait pointer MCPD_CONFIG_FILE. Les clés par projet,
l’api_key du juge et les en-têtes de transfert sont des
identifiants, donc le chart refuse de mettre dans un ConfigMap un contenu
qui en porte un : posez configFile.kind à Secret,
ou gardez le fichier dans un Secret que vous gérez et nommez-le dans
configFile.existingSecret. extraEnv,
extraVolumes et extraVolumeMounts couvrent le
reste, comme un paquet d’autorité de certification privée.
Chaque combinaison que le chart ne peut pas servir est refusée à l’installation, avec un message qui nomme la valeur à changer, au lieu de redémarrer en boucle ensuite. Le README du chart liste chaque refus.
deploy/helm/mcpd
# mcpd chart values.
#
# The chart refuses to install without keys. That is deliberate: a daemon on a
# cluster network with open ingest accepts writes from anything that can reach
# the Service, and a chart that lets you find that out later is not doing its
# job. Set keys.ingest and keys.read, or point keys.existingSecret at a Secret
# you manage yourself.
image:
repository: ghcr.io/mcpd/mcpd
# Empty means the chart's appVersion. Pin it in production.
tag: ""
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# Daemon configuration. Each of these maps to one environment variable.
config:
# The container port and the NetworkPolicy port are read from the number
# after the last colon, so moving the listener moves both.
listen: 0.0.0.0:4317
dataPath: /data/mcpd.db
project: default
# Days to keep signals. Empty or 0 keeps everything for ever, and the chart
# then sets no MCPD_RETENTION_DAYS at all.
retentionDays: 30
# Fraction of traces to keep, 0.0 to 1.0. Empty keeps all of them.
sample: ""
logFormat: json
# Turn off built-in redaction. Only sensible when something upstream already
# redacts.
noRedact: false
# Mask any value whose key contains one of these, on top of the built-in
# rules. Matched on lowercase substrings of the key.
redactKeyContains: []
# Longest single string stored, in bytes. Empty keeps the built-in 128 KiB.
redactMaxString: ""
# Where the run store lives, when runs.enabled is true. It must be inside the
# directory of dataPath, because that directory is the only writable mount.
# Empty derives it from dataPath: /data/mcpd.runs.db for the default. "off"
# is the same as runs.enabled false.
runsPath: ""
# Browser origins allowed to call the query API from another site, such as
# an interface served from a portal. Empty answers no cross-origin request.
corsOrigins: []
# Longest a request may take before it is answered with a 503. Empty keeps
# the built-in 30 seconds. Raise terminationGracePeriodSeconds with it.
requestTimeoutSeconds: ""
# An external database for signals, instead of the embedded file.
#
# An external store is what lets mcpd run more than one pod: the single writer
# and the single replica are limits of the SQLite file, not of mcpd. It is half
# of what it takes, though. The run store is always an embedded file (see
# `runs` below), so more than one replica also needs runs.enabled false.
#
# `type` decides the mode, never the URLs. That is what lets a URL live only in
# a Secret you manage: set the type here, point keys.existingSecret at the
# Secret, and leave the URL values empty. A URL value with type none, or with
# the other type, refuses to render rather than being ignored.
externalStore:
# none, postgres or clickhouse.
type: none
# A Postgres connection string. Reach for this first: it lifts both limits on
# a database most teams already run, back up and monitor.
postgresUrl: ""
# A ClickHouse HTTP endpoint. Reach for this when retention is measured in
# billions of spans.
clickhouseUrl: ""
clickhouseDatabase: mcpd
clickhouseUser: ""
# Both URLs and the ClickHouse password carry credentials, so they are held in
# the chart's Secret rather than in the pod spec. With keys.existingSecret,
# the chart creates no Secret: put them in yours under the keys postgres-url,
# clickhouse-url and clickhouse-password, and leave these values empty.
clickhousePassword: ""
# The durable run surface: definitions, the journal of step attempts, deadlines
# and waits.
#
# Its store is an embedded SQLite file whatever the signal store is, so while
# it is on, the pod keeps a file on a volume and runs as one replica. With an
# external store that volume holds the run store alone. Turn it off to run
# several pods against an external store; the daemon then answers the run
# endpoints with a 503 that says so, and nothing else changes.
runs:
enabled: true
# Pods to run. More than one needs both an external store and runs.enabled
# false, because each embedded SQLite file has one writer. The chart refuses to
# render a larger count otherwise, rather than pinning it to 1 behind your back.
replicaCount: 1
keys:
# Comma-separated keys permitted to write.
ingest: ""
# Comma-separated keys permitted to read stored prompts and completions.
read: ""
# Comma-separated keys permitted to change stored data: scores, feedback,
# datasets, experiments and acknowledgements. Unset means the ingest keys are
# accepted, which is what a deployment written before this scope existed did.
# The ingest key is in every container that emits a span, so separating them
# is worth doing once you have more than one team.
write: ""
# Comma-separated keys permitted to destroy data and read the audit log.
# Unset means the read keys are accepted, so a read key can delete a trace.
# `mcpd doctor` reports that as a warning and names this setting.
admin: ""
# Comma-separated keys permitted to start, cancel and signal runs. Unset
# means the write keys are accepted, and an admin key always is.
execute: ""
# Comma-separated keys permitted to claim steps and report their outcome.
# This is the key that lives in the process running step bodies. Unset means
# the write keys are accepted, and an admin key always is.
worker: ""
# Salt for the hashes redaction leaves behind. Optional, but set it anywhere
# that hashes: without it, digests of short identifiers can be guessed.
redact: ""
# Use a Secret you already manage instead of the values above. It must carry
# ingest-keys and read-keys, and optionally write-keys, admin-keys,
# execute-keys, worker-keys and redact-key.
existingSecret: ""
# The two bounds on one request. Empty means the built-in defaults, 33554432
# bytes (32 MiB) of body and 10000 signals per request.
#
# The byte cap is what bounds how much memory one request can take before
# anything is decoded, so lower it on a small pod. The signal cap is counted
# across the whole request on every ingest door, whether the signals arrive as
# one batch, an array of batches or an NDJSON stream.
maxBodyBytes: ""
maxBatchSignals: ""
# A ceiling on signals stored per project per minute. Empty means no ceiling,
# which is what a deployment that never sets it had before quotas existed.
#
# It stops one project spending the whole daemon. Signals over the ceiling are
# refused and reported with the count and the project, never dropped in silence,
# and the rest of the batch is still stored.
quotaPerMinute: ""
# Record WHICH PERSON acted on an audit row, not only which key.
#
# mcpd has no identity provider and is not getting one, so the identity comes
# from the ingress already in front of it. Both halves are required: the header
# your ingress sets, and the ranges allowed to set it. A header is a string
# anyone who can reach the port may send, so mcpd reads it only from an address
# you have named here. A request from anywhere else is served normally and its
# audit row carries no subject.
#
# Set trustedProxyCidrs to the range your ingress controller runs in, not to
# 0.0.0.0/0, which would trust every client to name itself.
identity:
# For example x-forwarded-user, x-forwarded-email or x-auth-request-user.
header: ""
# A list of CIDR ranges, for example ["10.42.0.0/16"].
trustedProxyCidrs: []
# The header your proxy puts the client's address in, for example
# cf-connecting-ip. Read only from trustedProxyCidrs. The sign-up and log-in
# limits count this address; without it they count the proxy's.
clientIpHeader: ""
# Open sign-up: anyone may create an account, which owns one new project and
# its own API keys. Off by default. On, a request with no key is refused
# everywhere except the health probes, sign-up and log-in. Set
# identity.clientIpHeader and identity.trustedProxyCidrs as well when an
# ingress is in front, so the per-address limits count clients.
signup:
enabled: false
# Where the accounts file goes. Empty puts it beside the data file, as
# mcpd.accounts.db, which is on the volume.
accountsData: ""
# Copy every stored span on to another OTLP endpoint. The local write always
# happens first, so a slow or absent destination never costs you the data.
forward:
# Base URL, not the /v1/traces path; mcpd appends it.
endpoint: ""
# Headers as "Name: value" strings. A credential here lands in the Secret.
headers: []
# The volume for whatever the pod keeps on disk: the signal store and the run
# store on the embedded store, or the run store alone with an external one.
# Not used with an external store and runs.enabled false, since nothing local
# is left to keep.
#
# With enabled false the chart mounts an emptyDir instead, so the daemon can
# still write, and everything it wrote is gone when the pod is.
persistence:
enabled: true
# Sized for the embedded signal store. A volume that holds the run store
# alone needs far less.
size: 20Gi
# Leave empty for the cluster default. SQLite locking is only correct on a
# local block device, so do not point this at NFS.
storageClass: ""
existingClaim: ""
service:
type: ClusterIP
port: 4317
annotations: {}
# Everything from here to `resources` is off by default, so an install that sets
# nothing renders the same four objects it always did. Each block is here
# because a chart review asks a question this chart could not otherwise answer.
# "What identity does this pod run as, and what can that identity do?"
serviceAccount:
create: false
# With create true, empty means the release's fullname. With create false,
# naming an account you manage yourself puts the pod on it, and leaving it
# empty keeps the namespace's `default` account, which is today's behaviour.
name: ""
annotations: {}
# mcpd never calls the Kubernetes API, so it has no use for a token.
# Turn this on only if something you add to the pod needs one.
automountServiceAccountToken: false
# "How does traffic reach mcpd from outside the cluster, and who terminates
# TLS?"
#
# Not mcpd: it speaks plain HTTP and has no TLS configuration, so the ingress
# controller has to terminate. Two things to know before you publish it.
#
# One path root. mcpd serves OTLP ingest, the query API and the operator
# interface from the same root, so a rule for "/" publishes all three, and your
# ingest keys are what stands between the open internet and a write. There is no
# path prefix that separates them.
#
# Body size. mcpd accepts a 32 MiB batch, and several ingress controllers cap a
# request body at 1 MiB by default, so a batch the daemon would have taken is
# refused before it arrives. On ingress-nginx the annotation to raise is
# `nginx.ingress.kubernetes.io/proxy-body-size`.
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: mcpd.example.com
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: mcpd-tls
# hosts:
# - mcpd.example.com
# "Only these namespaces may reach ingest."
#
# The limit belongs in the answer, so here it is. mcpd serves ingest, the query
# API, the operator interface and /metrics on ONE port, and a NetworkPolicy
# matches pods and ports, never paths. This gates the whole daemon or nothing:
# every source you allow in for ingest can also reach /v1/api and read stored
# prompts and completions. The read key is what holds those apart, not this
# policy. A split at the network layer would need a second listener, and mcpd
# does not have one.
#
# What it does do is still worth having. With no policy in the namespace, every
# pod in the cluster can open a connection to mcpd. With one, only these can.
#
# A trap worth knowing first: some CNI plugins enforce policy on traffic
# arriving from the node, and the kubelet's liveness and readiness probes
# arrive that way. On those clusters, add the node CIDR to `ipBlocks`, or the
# pod never becomes ready and the rollout stalls.
networkPolicy:
enabled: false
ingress:
# Whole namespaces, by label. Kubernetes sets
# `kubernetes.io/metadata.name` on every namespace itself, so this works
# without labelling anything first.
namespaceSelectors: []
# - matchLabels:
# kubernetes.io/metadata.name: agents
# Pods in mcpd's OWN namespace. A podSelector with no namespaceSelector
# beside it never reaches across a namespace boundary.
podSelectors: []
# - matchLabels:
# app.kubernetes.io/part-of: checkout-agents
ipBlocks: []
# - cidr: 10.42.0.0/16
# except:
# - 10.42.7.0/24
# Whole `from` entries, written out and appended as they are. Use this for
# the one thing the three lists above cannot say: a namespaceSelector and a
# podSelector inside a SINGLE entry, which means "those pods, in those
# namespaces" rather than "either of the two".
extraFrom: []
# - namespaceSelector:
# matchLabels:
# kubernetes.io/metadata.name: agents
# podSelector:
# matchLabels:
# app.kubernetes.io/name: checkout-agent
egress:
# Leave this off unless you have a reason. Restricting egress breaks four
# things and breaks them quietly, because mcpd logs a warning and carries
# on: alert webhooks, forwarding to another OTLP endpoint, the judge's model
# endpoint, and an external Postgres or ClickHouse.
enabled: false
allowDns: true
dnsNamespace: kube-system
# Complete egress rules, appended as written, because what mcpd is allowed
# to dial depends entirely on what you configured it to dial.
rules: []
# "What happens to ingest while a node is drained?"
#
# Only answerable with more than one replica, which takes an external store and
# runs.enabled false. With one replica, a budget over that pod either blocks
# every drain or permits every one. The chart refuses to render it there rather
# than let you discover which during a cluster upgrade.
podDisruptionBudget:
enabled: false
# Set one of these, never both. With replicaCount 2, minAvailable 1 lets one
# pod go at a time and keeps ingest up through a drain.
minAvailable: 1
maxUnavailable: ""
# "Is the collector itself monitored?"
#
# Needs the Prometheus Operator's CRDs in the cluster. Without them the install
# fails on a kind the API server does not know, which is why this is off rather
# than on with a capability check: a chart that silently drops the object you
# asked for is worse than one that fails.
#
# There is no credential below because mcpd serves /metrics with no key. It
# exposes counters and uptime, not stored prompts or completions.
serviceMonitor:
enabled: false
# Empty means the release's namespace.
namespace: ""
interval: 30s
scrapeTimeout: 10s
# The labels your Prometheus selects ServiceMonitors by. Without the ones it
# expects, this object is created and never scraped, and nothing reports that.
labels: {}
relabelings: []
metricRelabelings: []
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 512Mi
# The uid and gid are the ones the image creates its user with, so the pod and
# a plain `docker run` own files the same way. fsGroup is what makes a new
# volume, created owned by root, writable to that user. The seccomp profile is
# required by the Pod Security `restricted` level, which refuses the pod
# without it.
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
# Longer than the longest request. A request may run for the request timeout,
# 30 seconds by default, and the pod has to outlive it after SIGTERM or the
# kubelet kills it mid-write. Raise it with config.requestTimeoutSeconds.
terminationGracePeriodSeconds: 45
# The settings too structured for an environment variable: monitors, SLOs, the
# judge, per-project keys, retention and sampling under `projects`, and the run
# store's timings under `runs`. Written here as the JSON object the daemon's
# --config-file reads (see https://mcpd.sh/docs/configuration), mounted
# read-only at /etc/mcpd/mcpd.json, and named in MCPD_CONFIG_FILE. A change
# restarts the pod, as a key rotation does.
#
# Per-project keys, the judge's api_key and forward headers are credentials. A
# ConfigMap is readable by anything allowed to list ConfigMaps in the
# namespace, so the chart refuses to put content carrying any of them in one.
# Set kind to Secret, or keep the file in a Secret you manage and name it in
# existingSecret.
configFile:
content: {}
# monitors:
# - name: error-rate
# ...
# projects:
# checkout:
# retention_days: 90
# ConfigMap or Secret: the object `content` is rendered into.
kind: ConfigMap
# A Secret you manage that holds the file, instead of `content`.
existingSecret: ""
# The entry in existingSecret that holds the file.
existingSecretKey: mcpd.json
# Extra environment variables, as Kubernetes writes them, appended after the
# ones the chart sets. Use it for a setting this chart has no value for, or to
# read one from a Secret of your own with valueFrom.
extraEnv: []
# - name: MCPD_FORWARD_HEADERS
# valueFrom:
# secretKeyRef:
# name: otlp-credentials
# key: headers
# Extra volumes and where to mount them, for example a CA bundle for a
# ClickHouse or a judge endpoint behind a private certificate authority.
extraVolumes: []
extraVolumeMounts: []
podAnnotations: {}
nodeSelector: {}
tolerations: []
affinity: {} Avant que quoi que ce soit d’autre puisse l’atteindre
L’ingestion est ouverte par défaut. C’est un choix volontaire pour les cinq premières minutes sur un portable, où exiger une clé avant la première trace serait une friction sans bénéfice, et c’est le mauvais défaut dès l’instant où le port est joignable par autre chose. Le daemon le dit donc, chaque fois qu’il démarre sur une adresse qui n’est pas de bouclage sans clé :
$ docker run -p 4317:4317 -v mcpd-data:/data ghcr.io/mcpd/mcpd:0.1.0mcpd 0.1.0 listening on http://0.0.0.0:4317WARN listening on 0.0.0.0:4317 with open ingest. Anyone who can reach this port can write to it. Set --ingest-key (or MCPD_INGEST_KEYS) before exposing it.WARN listening on 0.0.0.0:4317 with an open read API. Stored prompts and completions are readable by anyone who can reach this port. Set --read-key to require a key. mcpd avertit plutôt que de refuser de démarrer, parce que refuser casserait le chemin qui le rend digne d’être exploité. L’avertissement est le contrat : si vous le voyez dans un journal de déploiement, ce déploiement est ouvert.
Six clés, six portées
--ingest-key permet d’écrire des signaux.
--read-key permet de les lire. --write-key permet
de changer des données stockées : scores, retours, datasets,
expériences et acquittements. --execute-key permet de démarrer, d’annuler et de signaler des exécutions. --worker-key permet de réclamer des étapes et de rapporter ce qui leur est arrivé ; c’est la clé qui vit dans le processus qui exécute vos étapes. --admin-key permet de supprimer
une trace et de lire le journal d’audit.
La première séparation est celle qui décide de votre rayon d’explosion, parce que les populations n’ont pas la même taille : chaque processus d’agent de votre flotte a besoin de la clé d’ingestion, et un nombre bien plus petit de personnes a besoin de l’une des autres. Donner à votre flotte une seule clé qui lit aussi mettrait un accès à chaque prompt stocké dans chaque conteneur qui émet un span, y compris ceux qui font tourner du code tiers.
C’est aussi pourquoi la capture d’une exécution dans un dataset est passée à la portée d’écriture. Elle lit au passage l’entrée et la sortie de l’exécution : l’autoriser avec la clé que porte votre flotte donnait donc à chacun de ces conteneurs un moyen de recopier n’importe quel prompt stocké.
Chaque option prend une liste séparée par des virgules et chacune a une
variable d’environnement : MCPD_INGEST_KEYS,
MCPD_READ_KEYS, MCPD_WRITE_KEYS et
MCPD_ADMIN_KEYS. Utilisez-en plusieurs : une clé par équipe ou
par flotte veut dire qu’une clé fuitée se révoque en la retirant de la
liste, et non en redéployant tout ce qui émet un span. Une clé est présentée
dans Authorization: Bearer … ou dans l’en-tête
X-Mcpd-Key, et elle est comparée en temps constant.
L’asymétrie à connaître est ce qui arrive quand vous laissez les deux
options les plus récentes non définies. Une liste d’écriture non définie
accepte les clés d’ingestion, et une liste d’administration non définie
accepte les clés de lecture : une mise à niveau ne révoque donc jamais une
clé qui marchait. Tant que vous ne posez pas --admin-key, une
clé de lecture est une clé pour lire et pour supprimer, pas une clé
strictement en lecture seule, et le daemon le dit au démarrage. Distribuez-la
en conséquence, ou posez l’option.
Qui a lu quoi
Les gestionnaires qui renvoient des payloads enregistrent l’accès : le détail d’une trace, les recherches de spans et de logs, les retours, les éléments d’un dataset, la capture et la suppression. Une ligne porte l’heure, une empreinte courte de la clé qui a agi, l’action, le projet, la ressource et si l’autorisation l’a permis. La clé elle-même n’est jamais notée : le journal n’est donc pas un second endroit où vivent vos secrets.
Lisez-le avec GET /v1/api/audit, qui demande la portée
d’administration. Deux propriétés valent d’être connues avant de compter
dessus. Il n’est pas élagué par --retention-days, parce qu’un
journal qui se supprimerait au même rythme que les données qu’il décrit ne
pourrait pas dire qui a lu les données désormais parties. Et écrire une ligne
ne fait jamais échouer la lecture qu’elle décrit : une ligne qui ne peut pas
être écrite est comptée dans
mcpd_audit_write_failures_total sur /metrics, qui
est le compteur sur lequel alerter.
Ce qu’une clé ne couvre pas
- L’interface. Le balisage servi sur
/l’est à quiconque peut atteindre le port. Les données derrière ne le sont pas : chaque requête qu’elle fait passe par l’API de requête, qui applique la clé de lecture. Il n’y a pas d’écran de connexion dans le daemon : si ce sont des personnes plutôt que des services qui ont besoin d’un accès, placez-le derrière le proxy d’identité que vous exploitez déjà. - Les sondes et
/metrics. Sans authentification, à dessein, parce qu’un kubelet et un collecteur Prometheus ne peuvent pas porter de clé. Ils exposent des compteurs et une durée de fonctionnement, jamais des payloads. - Le transport. mcpd parle du HTTP simple et ne termine pas le TLS. Terminez-le à votre ingress ou dans votre maillage. Les prompts et les complétions traversent ce lien en entier.
- Les empreintes dans les payloads masqués. Le masquage
est actif sauf si vous passez
--no-redact, et là où il hache un identifiant, l’empreinte n’est privée que dans la mesure où son sel l’est. Posez--redact-key, sinon des identifiants courts peuvent être retrouvés en devinant. Voir Masquage et conservation.
Vérifiez avant de l’exposer
mcpd config affiche la configuration qu’une invocation
utiliserait, puis quitte. C’est le moyen le moins coûteux de découvrir que la
variable d’environnement que vous avez posée au mauvais endroit n’a rien
fait :
$ docker compose -f deploy/docker/docker-compose.yml run --rm mcpd configlisten 0.0.0.0:4317storage sqlite at /data/mcpd.dbconfig file nonelog format jsonproject defaultprojects noneretention 30 daysingest auth key requiredread auth key requiredwrite auth key required (ingest keys)admin auth key required (read keys)execute auth key required (ingest keys)worker auth key required (ingest keys)signup offruns sqlite at /data/mcpd.runs.dbrun settings lease_ttl_ms 30000, reclaim_window_ms 30000, stall_window_ms 300000, idempotency_window_ms 86400000, park_ttl_ms 86400000, max_depth 8, max_step_repeats 16identity offclient ip peer addresssampling keep everythingquota unlimitedredaction onredact key setcors same-origin onlytimeout 30s per requestlimits 32 MiB per request, 10000 signals per batchderived duration and token metricsaspects offmonitors noneobjectives nonejudge offforwarding off
Six lignes comptent ici. Si une ligne d’authentification affiche
open, cette portée est ouverte à quiconque peut atteindre le
port. Une ligne entre parenthèses nomme la liste plus large dont cette portée
hérite : l’invocation ci-dessus ne pose pas de --admin-key, donc
ses clés de lecture suppriment autant qu’elles lisent. Elle ne pose pas non
plus de --execute-key ni de --worker-key, donc ses
clés d’ingestion lancent aussi des exécutions et réclament des étapes, et
mcpd le signale au démarrage. Chaque option qu’elle
affiche est documentée dans
Configuration.
Sauvegardes
La sauvegarde est un seul fichier. Tout ce que mcpd stocke, index de
recherche compris, est dans le fichier à MCPD_DATA.
Copier ce fichier pendant que le daemon tourne est la seule chose à ne pas
faire. mcpd fait tourner SQLite en mode WAL : les écritures récentes vivent
donc dans un fichier -wal à côté, et une copie de la base seule
est une copie à laquelle elles manquent. Prenez plutôt une copie cohérente,
depuis n’importe quelle image qui a l’outil sqlite3 et qui
monte le même volume :
sqlite3 /data/mcpd.db ".backup '/backup/mcpd-$(date -u +%Y%m%dT%H%M).db'"
Les deux lisent un instantané cohérent pendant que les écritures continuent.
Un instantané de volume pris par votre pilote CSI marche aussi, si
l’instantané du pilote est atomique ; un tar d’un répertoire
vivant, non. Restaurer, c’est arrêter le daemon, remettre le fichier à
MCPD_DATA, supprimer les fichiers -wal et
-shm à côté, et redémarrer le daemon ;
Exploitation le déroule
pas à pas.
Les lignes supprimées libèrent des pages dans le fichier, elles ne le
réduisent pas : une base qui a fait vieillir beaucoup de données garde donc
la taille qu’elle avait. Lancez VACUUM quand vous voulez
récupérer le disque.
Mises à niveau
Changez l’étiquette et redémarrez. Les migrations tournent au lancement et peuvent être jouées à chaque lancement sans danger : aucune étape ne se passe donc hors du processus. Prenez tout de même la sauvegarde d’abord : une migration est le seul moment où un schéma change sous vos données.
Le redémarrage est une courte interruption, pas une mise à jour progressive, pour la même raison qu’il n’y a qu’une réplique. Les agents y survivent : les SDK mettent en file en mémoire et réessaient, et la file est bornée, si bien qu’un redémarrage de quelques secondes ne coûte rien et qu’un redémarrage d’une heure jette les spans les plus anciens et le dit dans le journal du client.
mcpd traite SIGTERM et s’arrête proprement, ce qui est ce qu’envoie
Kubernetes. terminationGracePeriodSeconds doit durer plus
longtemps que la plus longue requête, sinon le kubelet tue le processus au
milieu d’une écriture. Une requête peut durer jusqu’au délai de 30 secondes,
donc les manifestes et le chart donnent 45 secondes au pod, et le fichier
compose en donne autant au conteneur. Augmentez les deux ensemble.
Où s’arrête un seul fichier SQLite, et quoi faire ensuite
Le fichier embarqué est le bon défaut et il a deux limites dures. Les deux appartiennent à SQLite, pas à mcpd.
- Un seul écrivain. Les lectures tournent sur leurs propres connexions et ne bloquent jamais l’écrivain, ce à quoi sert le mode WAL, mais un seul processus possède le fichier. Cette instance se dimensionne vers le haut, pas vers les côtés.
- Un volume, une zone de disponibilité. Une revendication ReadWriteOnce fixe le pod. Il n’y a pas de bascule vers un autre nœud avec les données intactes.
Deux réglages rapportent en général plus qu’un nouveau backend. Échantillonnage garde une fraction des traces et garde entière chaque trace gardée, en décidant par hachage de l’identifiant de trace pour que toutes les instances d’une flotte s’accordent. La conservation borne jusqu’où le fichier remonte. Faire tourner une instance par équipe, chacune avec son volume, marche aussi et empêche une équipe bruyante de remplir le disque de tout le monde.
Quand ces réglages sont épuisés, pointez mcpd sur une base. Posez
externalStore.type à postgres ou
clickhouse dans le chart, avec la chaîne de connexion dans
externalStore.postgresUrl ou
externalStore.clickhouseUrl. La chaîne est tenue dans le Secret
du chart, pas dans la spécification du pod. Pour la garder entièrement hors
de vos valeurs, mettez-la dans un Secret que vous gérez, sous
postgres-url ou clickhouse-url, nommez ce Secret
dans keys.existingSecret, et posez le type malgré tout : c’est
le type, et non l’URL, qui choisit le backend.
Cela déplace les signaux et laisse un fichier sur disque : le magasin
d’exécutions, qui est un SQLite embarqué quel que soit le magasin des
signaux. Avec runs.enabled à true, la valeur par défaut, le
chart le garde sur le volume et fait tourner une réplique. Pour faire
tourner plusieurs pods, coupez la surface d’exécutions. Le chart fait alors
tourner replicaCount pods, passe de Recreate à une
mise à jour progressive, et abandonne le PersistentVolumeClaim, parce qu’il
ne reste rien de local à se disputer, et le daemon répond aux endpoints
d’exécutions par un 503 qui le dit. Le chart refuse un
replicaCount supérieur à 1 tant que les exécutions sont
actives, plutôt que de démarrer des pods qui tiendraient chacun leur propre
magasin d’exécutions.
helm install mcpd ./deploy/helm/mcpd \
--set keys.ingest=$INGEST_KEY \
--set keys.read=$READ_KEY \
--set externalStore.type=postgres \
--set externalStore.postgresUrl=postgres://mcpd:$PGPASSWORD@db:5432/mcpd \
--set runs.enabled=false \
--set replicaCount=3 Prenez Postgres en premier : il lève les deux limites sur une base que la plupart des équipes exploitent, sauvegardent et surveillent déjà. La migration prend un verrou consultatif et est sautée quand le schéma est à jour : plusieurs répliques qui démarrent ensemble sont donc sans danger. Prenez ClickHouse quand la conservation se mesure en milliards de spans. Une URL pour un backend que le type ne nomme pas est refusée au lieu d’être ignorée.
Une chose ne change pas avec le backend.
La conservation couvre les projets que vous avez écrits. La
passe horaire supprime les signaux plus vieux que
--retention-days dans le projet nommé par
--project et dans chaque projet du bloc projects,
chacun sur son propre retention_days là où il en pose un. Les
traces envoyées sous un nom de projet qui ne figure ni dans l’un ni dans
l’autre n’ont pas de fenêtre et ne sont jamais élaguées : surveillez donc le
volume si vos agents posent le leur.
Ensuite
- Toutes les options et variables d’environnement : Configuration.
- Sondes, compteurs et exploitation au quotidien : Exploitation.
- Ce qui n’atteint jamais le disque : Masquage et conservation.
- Rien de déployé pour l’instant : Prise en main.