Every value of the easyp-service Helm chart 1.0.2, with the chart's own description.
Every value in values.yaml of the easyp-service chart 1.0.2 (app v1.0.2),
117 in total. Descriptions are the chart's own comments, quoted as written, which is why they are in
English on both language versions of this page. What must be set and what is dangerous is summarised in
Installation; config.* values map to the
service configuration. The chart mostly comments whole blocks, so a key
marked — is covered by the paragraph above its table or by the notes below it.
The same file, for the version you install:
helm show values oci://ghcr.io/easyp-tech/charts/easyp-service --version 1.0.2
The file opens with:
Default values for easyp-service. Three things have no working default and must be decided before the chart will install. Each is refused at install time rather than at runtime, so a missing one is a message naming it, not a pod that comes up wrong. 1. A database DSN — see secrets. There is nothing sensible to default to. 2. TLS material, because tls.enabled defaults to true. Supply one of: tls.serverSecret=<kubernetes.io/tls Secret> you bring the certificate certManager.enabled=true the chart issues it tls.enabled=false plaintext, trusted network The default stays on: a listener that speaks plaintext is the wrong thing to arrive at by saying nothing. But it does mean there is no install that consists only of a DSN, and this list said otherwise until v0.14.0. 3. ingress.host, and with it config.server.trustedProxies, if you want the gRPC API reachable from outside the cluster. Both are required together — see the comment on trustedProxies for why the second one is not optional.
The service reads its certificate and key once, at startup, so a rotated certificate is not picked up until the pod restarts. With stakater/Reloader installed this annotation restarts the deployment when the secret changes. Without it, rotation is a manual kubectl rollout restart.
Sized against what the service is configured to do, not guessed. The plugin's output is read into memory whole, so peak usage tracks config.workerPool.maxConcurrentGenerations × config.registry.maxOutputSize — 16 × 64 MiB = 1 GiB of buffers with the defaults below, and the same bytes again once marshalled into the gRPC response. _helpers.tpl refuses an install where that product no longer fits in the limit. The earlier default was 1Gi, which the buffers alone filled. The pod was OOMKilled under load, and an OOMKill reads as a crash rather than as overload: no log line, no rejected-generation counter, no saturation alert.
Key
Type
Default
Description
resources.requests.cpu
string
"500m"
—
resources.requests.memory
string
"1Gi"
—
resources.requests.ephemeral-storage
string
"1Gi"
Writable layer and logs only. Both the plugin cache and the archives staged mid-download live on the plugins volume, not here — the download used to land in /tmp on the writable layer, where a few hundred megabytes times the concurrent generations was an eviction rather than an overload. With persistence disabled see persistence.ephemeralSizeLimit, which is what actually bounds that volume.
resources.limits.cpu
string
"4"
Four cores for sixteen concurrent plugin processes. On two they each ran at an eighth speed and approached generationTimeoutSeconds, which would have surfaced as DeadlineExceeded — a broken plugin, to anyone reading — rather than as the honest ResourceExhausted the limiter returns.
The MCP endpoint: a read-only HTTP surface AI tooling uses to read the plugin catalog and the easyp.yaml schema. It exposes nothing the anonymous gRPC reads do not, but it sits outside the gRPC interceptor chain — no TLS, no rate limit, no audit — so serving it is a deployment's explicit decision. Off: the container does not listen on ports.mcp and the Service does not publish it. Turn it on inside a trusted network or behind an ingress that terminates TLS.
Seconds Kubernetes waits after SIGTERM before SIGKILL. Three numbers have to line up, and _helpers.tpl enforces the order: generationTimeoutSeconds < forceShutdownAfterSeconds < terminationGracePeriodSeconds A generation the service accepted must be able to finish; the process must be able to exit on its own after that; and Kubernetes must wait for both rather than reaching for SIGKILL first.
Configuration that is not secret. Rendered into a ConfigMap and mounted at /etc/easyp/config.yml — the same config file every other deployment of this service reads. Secrets are not here; they arrive as environment variables from the secret above and override the file. The numbers below repeat the defaults compiled into the service, on purpose: this is the chart's documented interface and helm show values is where an operator looks them up. tests/render.sh checks that they still agree with the binary, so a default changed on one side and not the other fails there rather than in a cluster.
Key
Type
Default
Description
config.logLevel
string
"info"
—
config.server.trustedProxies
list
[]
CIDRs whose X-Forwarded-For and X-Real-IP headers may be believed. Required once ingress.enabled is true, and the chart refuses the install without it. Behind a proxy every request arrives from the proxy's address, so with this empty the rate limit and the per-caller concurrency limit become one bucket shared by every client at once, and the audit log records the ingress instead of who acted. None of that fails visibly. Set it to the range your ingress controller's pods run in — the pod CIDR of that node pool, not the whole cluster: anything listed here can claim to be any client.
config.server.maxRecvMsgSize
integer
67108864
Largest single gRPC message accepted and sent. gRPC's own defaults are 4 MiB in — which a request carrying a large proto tree exceeds — and effectively unlimited out. maxSendMsgSize must be at least config.registry.maxOutputSize or the service refuses to start: a plugin allowed to produce more than can be sent does its work for nothing.
config.server.maxSendMsgSize
integer
67108864
—
config.server.maxConcurrentStreams
integer
256
Concurrent streams one connection may hold. gRPC defaults to unlimited, which lets a single caller allocate goroutines and buffers until the pod is out of memory, before any limiter in the chain sees the requests.
config.forceShutdownAfterSeconds
integer
150
Hard exit this long after SIGTERM, if graceful shutdown has not finished. Expressed in seconds so the chart can compare it with the two numbers on either side of it.
config.workerPool.workers
integer
4
Concurrent plugin lookups: a database read, plus a download and unpack from object storage on a cache miss.
config.workerPool.queueSize
integer
16
Waiting slots, applied to lookups and to generations alike. Beyond this the answer is ErrServerOverloaded rather than a longer queue.
config.workerPool.maxConcurrentGenerations
integer
16
Concurrent plugin processes. Separate from workers on purpose: a worker is released once the plugin is located, and execution runs after that.
config.workerPool.generationTimeoutSeconds
integer
120
Expressed in seconds (not "120s") so the chart can compare it against terminationGracePeriodSeconds. Rendered as a Go duration for the service.
config.workerPool.maxRetries
integer
2
—
config.workerPool.shutdownTimeout
string
"30s"
—
config.rateLimit.requestsPerSecond
integer
10
—
config.rateLimit.burst
integer
20
—
config.rateLimit.cleanupInterval
string
"10m"
—
config.rateLimit.maxConcurrentPerIP
integer
2
Requests one client may have in flight at once. Rate alone does not bound this: a caller staying under the rate can still hold every generation slot with long requests. 0 disables the check.
config.audit.bufferSize
integer
1000
—
config.audit.batchSize
integer
100
—
config.audit.flushInterval
string
"1s"
—
config.audit.maxSaveRetries
integer
3
—
config.audit.enqueueTimeout
string
"1s"
How long an operation waits for room in the audit queue. This is the only place audit can slow a request down, and it takes a backed-up queue to get there: a healthy writer drains batchSize/flushInterval — a hundred entries a second — so this expires only when the database has stopped keeping up. An entry that never finds room is dropped and counted under easyp_audit_events_lost_total{reason="enqueue_timeout"}; the operation itself still succeeds.
config.audit.flushTimeout
string
"5s"
Bound on one write to storage, retries included. A batch that misses it is lost rather than holding the writer up behind a database that has stopped answering.
config.audit.retentionMonths
integer
12
Partitions older than this are dropped on a schedule — a real delete, so afterwards the only copy of that month is in a backup taken while it still existed. Backup retention has to exceed this rather than match it: matching means the month leaves the database and the archive at about the same time, leaving it nowhere.
config.audit.preCreateMonths
integer
3
—
config.audit.partitionCheckInterval
string
"6h"
—
config.audit.partitionOpTimeout
string
"30s"
—
config.registry.pluginsDir
string
"/plugins"
—
config.registry.maxOutputSize
integer
67108864
—
config.registry.cacheMaxBytes
integer
21474836480
Bound on the unpacked plugins on disk. Least recently used ones are dropped once this is exceeded; the archive in object storage is left alone, so an evicted plugin is one download away rather than lost. Keep it below persistence.size — eviction starts at the limit, and the volume needs room to reach it. 0 disables eviction. 20 GiB
config.registry.s3.endpoint
string
""
—
config.registry.s3.bucket
string
""
—
config.registry.s3.region
string
"us-east-1"
—
config.registry.s3.prefix
string
""
—
config.registry.s3.forcePathStyle
boolean
false
—
config.telemetry.otlpEndpoint
string
""
Empty means no collector, and the service then builds no exporter at all. These carry no fallback on purpose: the OTLP connection is lazy, so an endpoint nobody is listening on does not fail — it retries forever, and a pod with no observability stack would fill its log with connection errors while otherwise working.
config.telemetry.pyroscopeEndpoint
string
""
—
config.telemetry.serviceTier
string
""
Tags traces and profiles with the licence tier this release serves. Worth setting only where community and enterprise run side by side and their signals land in the same backends; on a cluster running one tier it would distinguish nothing. Metrics carry the same distinction already, so use the same word here that the metric label uses.
Public halves of the keys licence tokens are signed with, keyed by the key id that appears in the token footer. The token names one; the rest are here so that a signing key can be rotated without every deployment having to change key on the same day. These are the trust anchor: whoever sets them decides which authority may issue licences for this installation. The default below is easyp.tech's own published key, so a customer holding a LICENSE_KEY needs nothing else. Replace it only if you issue your own licences. Source of truth is keys/ in the licence registry (easyp-tech/licenses); these must be kept in step with it. Without a key, LICENSE_KEY is ignored and the service runs in community mode.
config.registry.s3: Leaving bucket empty disables S3 and makes pluginsDir the only source of plugin binaries.
Secrets The pod loads them with envFrom, so the KEYS OF THE SECRET ARE THE ENVIRONMENT VARIABLE NAMES. A secret you bring yourself must use exactly these keys: DB_POSTGRES_DSN required REGISTRY_S3_ACCESS_KEY_ID required when config.registry.s3.bucket is set REGISTRY_S3_SECRET_ACCESS_KEY required when config.registry.s3.bucket is set LICENSE_KEY optional; absent means community mode The single-key LICENSE_PUBLIC_KEY was removed in v0.13.0. The trust anchor is config.license.publicKeys alone: an entry per key id, or one under "*" to verify any key id. Without a public key, LICENSE_KEY counts for nothing. AUTH_WRITE_TOKENS optional; absent means no writes are allowed AUTH_WRITE_TOKENS holds sha256 digests, never the tokens: "ci=<64 hex>,me=<64 hex>". Generate a token with easyp-svc auth new-token --name ci.
Key
Type
Default
Description
secrets.existingSecret
string
""
Bring your own secret (recommended). Values put in data below end up in Helm release history and in helm get values, which is why create defaults to false.
Transport security When enabled, the gRPC listener serves TLS. Adding clientCASecret turns it into mutual TLS: only callers holding a certificate signed by that CA get in, which is what keeps the listener private when the ingress is the only party meant to reach it.
Key
Type
Default
Description
tls.enabled
boolean
true
—
tls.serverSecret
string
""
Secret of type kubernetes.io/tls holding tls.crt and tls.key for the server.
tls.clientCASecret
string
""
Secret holding ca.crt used to verify client certificates. Empty falls back to ca.crt inside serverSecret, which is what cert-manager writes for a CA issuer. Set to "-" to serve plain server-side TLS with no client check.
Ingress. Defaults target Traefik because it is the only controller that can express a mutual-TLS backend leg declaratively, via ServersTransport.
Key
Type
Default
Description
ingress.enabled
boolean
false
—
ingress.className
string
"traefik"
—
ingress.host
string
""
—
ingress.annotations
map
{}
—
ingress.tlsSecret
string
""
Secret holding the certificate served to external clients.
ingress.serversTransport.enabled
boolean
true
—
ingress.serversTransport.clientSecret
string
""
Secret of type kubernetes.io/tls with the client certificate the router presents to the service.
ingress.serversTransport.serverName
string
""
Name the router expects on the service certificate.
ingress.serversTransport.insecureSkipVerify
boolean
false
—
ingress.serversTransport: Traefik ServersTransport describing how the router reaches the pod. Required whenever tls.enabled is true, otherwise the backend leg would be plaintext against a listener that demands a client certificate.
Storage for the plugin cache. Plugin archives are downloaded lazily and unpacked here, and the directory is pruned: once it passes config.registry.cacheMaxBytes the least recently used plugin versions are removed. So size this for the working set you want resident, not for every plugin that could ever be requested. Eviction only ever removes local files. The archive stays in object storage, so an evicted plugin costs one download on its next request rather than being lost — which is why the cache can be sized to taste. Whichever of the two sizes below applies has to exceed cacheMaxBytes, and the chart refuses to install when it does not: eviction starts at the limit, so storage sized exactly to it is already full by the time the cache first needs headroom.
Key
Type
Default
Description
persistence.enabled
boolean
true
—
persistence.existingClaim
string
""
—
persistence.storageClass
string
""
—
persistence.accessMode
string
"ReadWriteOnce"
—
persistence.size
string
"25Gi"
—
persistence.ephemeralSizeLimit
string
"25Gi"
Used when enabled=false, where the cache lives in an emptyDir on the node instead of a volume of its own. Without a limit here the cache grows into the node's ephemeral storage, and disk pressure there is resolved by the kubelet evicting pods — not necessarily this one. With it, an overrun stops at the pod that caused it. Remember this is per replica: with persistence off, every pod keeps its own copy, so plan for replicaCount × this much on the nodes.
Alerting rules. Needs the Prometheus Operator CRDs, same as serviceMonitor, which is why this defaults to off rather than failing the install where they are absent. Turn it on wherever you actually run this. The rules worth having on day one are the licence ones: a licence that lapses breaks nothing loudly. The tier drops to community, audit stops, and the plugin limit starts rejecting registrations — all silently.
Key
Type
Default
Description
prometheusRule.enabled
boolean
false
—
prometheusRule.labels
map
{}
Extra labels, usually what your Prometheus selects rules by.
Where each alert's runbook_url points. Every alert gets one, anchored on its own name. Point this at your own copy if you keep runbooks internally — an alert that arrives at 3am with no procedure attached is where most of the time goes.
prometheusRule.licenceExpiryWarningDays
integer
14
How long before expiry to start warning. The token also carries its own grace period, which runs after this; both exist so a renewal that slips does not take a customer's pipeline down.
prometheusRule.cacheUsageWarningRatio
number
0.95
Fraction of config.registry.cacheMaxBytes that counts as full.
prometheusRule.generationErrorRatio
number
0.05
Fraction of generations allowed to fail before alerting.
prometheusRule.authFailureRate
number
0.5
Rejected write credentials per second before alerting.
On by default. This pod's whole job is running third-party binaries, so the set of places it can reach should be stated rather than inherited. If your database, object storage or collector is not on one of the ports below, add it here or the pod goes quiet in a way that looks like a hang. That is the one thing to check first after enabling this.
Key
Type
Default
Description
networkPolicy.enabled
boolean
true
—
networkPolicy.ingressNamespaceSelector
map
{}
Selector matching the namespace your ingress controller runs in. Empty means the gRPC port accepts connections from anywhere in the cluster — set it once you know which namespace your controller runs in.
networkPolicy.egressPorts
list
[5432,443,4317]
Ports the pod is allowed to reach outbound, beyond DNS.