Two charts live here:
jentic-one/— the umbrella application chart: one subchart per service (app,broker,registry,admin,control), an nginxgatewayfor the parts topology, a bundledpostgresqlfor dev/trial, and acommonlibrary chart of template helpers. Release-scoped resources (generated secrets, the migrate hook) live in its owntemplates/.observability/— a standalone LGTM stack (Grafana/Loki/Tempo/Prometheus + OTel collector) for local dev.
values/ holds the per-mode and per-overlay value files the local
workflow composes.
Installing on a real cluster? That journey is
docs/installation/helm.md — including
the secrets model (generated vs your own). This page is the chart-development
and smoke-test workflow.
- The chart is not published to any registry (the AWS Marketplace ECR
copy is the exception — see
marketplace publishing).
Vendor
deploy/helm/jentic-one/from a checkout at the release tag. - Subcharts default to locally-built
jentic-one/<svc>image repos. Only theappsubchart can be pointed at the published GHCR image (--set app.image.repository=… --set app.image.tag=…); thebrokersubchart doesn't setJENTIC__APPS=brokeritself, so running it on the published app image needsbroker.extraEnv.JENTIC__APPS=broker(the AWS Marketplace overlay does exactly that). - No
securityContexton any application pod (norunAsNonRoot,readOnlyRootFilesystem, orcapabilities.drop) — only the bundled Postgres StatefulSet sets one. Hardening per docs/security/README.md currently means patching the chart or applying a cluster policy. - No Ingress, NetworkPolicy, PodDisruptionBudget, HorizontalPodAutoscaler, or anti-affinity anywhere in the chart — TLS termination and HA shaping are yours to bring.
global.observability.logging.{format,level}are dead values (see Observability below).- Omitting
global.image.tag(and per-service tags) silently falls back to:latest— no warning is printed, despite the comment in_image.tpl.
A kind-based local cluster validates that chart installs work, pods become Ready, and health endpoints respond — without a cloud environment.
- kind (v0.20+), Helm (v3.12+),
kubectl(kind manages kubeconfig) - Docker images built locally (
make build-all)
| Mode | Values file | What runs |
|---|---|---|
combined |
values/local-combined.yaml |
app (all surfaces) + broker + PostgreSQL |
parts |
values/local-parts.yaml |
registry + admin + control + broker + gateway + PostgreSQL |
broker |
values/local-broker.yaml |
broker + PostgreSQL |
make build-all # Build all Docker images
uv run python -m tools.deploy cluster up # Create local kind cluster
uv run python -m tools.deploy up --mode combined # Deploy with combined mode
curl http://localhost:8000/health # Verify app health
uv run python -m tools.deploy smoke # Run smoke test suite
uv run python -m tools.deploy down # Remove the release
uv run python -m tools.deploy cluster down # Destroy the clusterAdd --otel to up to wire app OTel sidecars to the observability stack
(requires obs up first). To switch modes, down then up --mode parts.
Running a hosted Jentic install alongside this one? A client is bound to one
backend at a time — confirm which backend a base URL serves with
curl http://localhost:8000/instance
(local/remote coexistence).
tools.deploy up loads only the images the chosen --mode needs into the
kind cluster:
--mode |
Images loaded |
|---|---|
combined |
app, broker |
parts |
registry, admin, control, broker |
broker |
broker |
The bundled PostgreSQL uses the official postgres image from Docker Hub —
nothing to build. To pre-warm every service image once (useful when switching
modes), run uv run python -m tools.deploy load --all after cluster up.
uv run python -m tools.deploy logs app # Tail app pod logs
uv run python -m tools.deploy logs broker # Tail broker pod logsFrom deploy/k8s/kind-config.yaml, kept in sync
with the service.nodePort values in the local overlays:
| Host port | Service (mode) | Kind nodePort |
|---|---|---|
| 8000 | app (combined) / gateway (parts) | 30080 |
| 8080 | broker (all modes) | 30081 |
| 8001 | admin (parts) | 30082 |
| 8002 | control (parts) | 30083 |
| 8003 | registry (parts) | 30086 |
The smoke-helm workflow runs the
mode matrix via the same tools.deploy ci-smoke entrypoint on an ephemeral
kind cluster: post-merge on every push to main (path-filtered off doc-only
commits), as a blocking release gate (release.yml calls it on every
vX.Y.Z tag and refuses to publish until the matrix is green), and on
manual dispatch for a single mode.
Two layers, deployed independently.
brew install k9s
uv run python -m tools.deploy ui # k9s scoped to the jentic namespaceA terminal UI — pods, services, logs, exec, port-forward. Fastest way to see
what's happening during tools.deploy up.
observability/ deploys the full set into a monitoring
namespace, with two independent pipelines:
- Traces + metrics — apps emit OTLP from a sidecar to the central OTel Collector, which fans out to Tempo and Prometheus.
- Logs — a Grafana Alloy DaemonSet on each node tails
/var/log/pods/...and pushes JSON-parsed lines (withlevel,trace_id,span_idextracted) directly to Loki. No app-side change; logs keep going to stdout.
app pod ─┐
broker ─┼─ otel sidecar ──► obs-otelcollector ─┬─► Tempo (traces)
… ┘ └─► Prometheus (metrics)
▼
node /var/log/pods ──► Alloy (DaemonSet) ─────► Loki ─► Grafana (UI)
The lanes meet only in Grafana, which correlates them by trace_id (the
Loki datasource's derived fields). Subchart deps come from the upstream
community charts (grafana/loki, grafana/tempo, grafana-community/grafana,
prometheus-community/prometheus, open-telemetry/opentelemetry-collector,
grafana/alloy).
uv run python -m tools.deploy obs up # Install into monitoring/
uv run python -m tools.deploy up --mode combined --otel # App sidecars point at obs-otelcollector
uv run python -m tools.deploy grafana # Port-forward Grafana to localhost:3000
uv run python -m tools.deploy obs down # Tear it downLogs flow into Loki regardless of --otel (they come from Alloy, not the
sidecars). Default Grafana credentials are admin / admin; datasources for
Prometheus, Loki, and Tempo are pre-wired, with Trace → logs /
Trace → metrics links.
Knobs worth knowing:
- Resource sizing — defaults in
values/local-observability.yamlkeep the stack under ~2 GiB on a single kind node; persistence is off. - No
--otel— apps run with no sidecar, no overhead; logs still flow. - Adding a component — add a subchart to
observability/Chart.yaml; it's a normal Helm umbrella.
This stack is for local dev / smoke. For production, run the same charts against persistent storage (e.g. S3 for Tempo/Loki), behind real auth (OIDC for Grafana), and consider kube-prometheus-stack for alertmanager + node-exporter + service monitors.
Subcharts include built-in support for structured logging and an OTel
sidecar; see _logging.tpl
and _otel-sidecar.tpl.
OTEL_SERVICE_NAME(<release>-<chart>, e.g.jentic-broker) is injected into every service pod. The template also injectsLOG_FORMATandLOG_LEVEL, but nothing in the application reads them — the real knobs are the config keysruntime.log_levelandruntime.debug(viaextraEnv:JENTIC__RUNTIME__LOG_LEVEL,JENTIC__RUNTIME__DEBUG); the chart valuesglobal.observability.logging.{format,level}are dead. The defaults coincide (JSON at info), which is why this goes unnoticed.- With
global.observability.otel.enabled=true, each pod gets an OTel Collector sidecar receiving OTLP gRPC onlocalhost:4317and exporting to the configured endpoint:
helm install jentic deploy/helm/jentic-one \
--set global.observability.otel.enabled=true \
--set global.observability.otel.endpoint=http://otel-collector:4317The application emits metrics via the OpenTelemetry metrics SDK. The
exporter is chosen at deploy time
(JENTIC__OBSERVABILITY__METRICS__EXPORTER /
global.observability.metrics.exporter):
| Exporter | How it works | When to use |
|---|---|---|
otlp |
Pushes to OTel Collector on localhost:4317 (sidecar) | Production / OTel-based stacks (default) |
prometheus |
Exposes /metrics endpoint on the service port |
Environments with Prometheus scraping |
none |
No-op — SDK inactive | Tests, CI, local dev without obs stack |
Mind the default pairing: exporter: otlp with otel.enabled: false (also
the default) leaves the SDK pushing at a collector that isn't there — an
export-failure log line every interval. Either enable the otel sidecar or set
the exporter to none.
For Prometheus scraping, layer the annotation overlay — the observability chart's Prometheus auto-discovers annotated pods:
uv run python -m tools.deploy up --mode combined
helm upgrade jentic deploy/helm/jentic-one \
-f deploy/helm/values/local-combined.yaml \
-f deploy/helm/values/local-prom-app.yamlAdding custom instruments in app code? Go through get_meter() in
src/jentic_one/shared/metrics.py
(enforced by tests/arch/test_metrics_facade.py) and keep label cardinality
bounded — route templates and enums, never raw paths or user-supplied IDs.