Install on EKS + S3¶
Verdigris ships as a single container image and a Helm chart, so a production install is
one helm install. Data lands in your own S3 bucket; the pods are stateless and scale
freely. This page is the full production reference — the Quickstart is
the fast path from zero to logs in S3.
The whole promise
One binary, helm install, done — running against your bucket, with no static keys
in the cluster. The same image runs the local demo and the EKS deployment; only the
config differs.
Build & push the image¶
The multi-stage Dockerfile at the repo root builds the production web UI (web/ — Vite +
SolidJS), compiles vdg --features serve (DataFusion + axum), and ships a slim non-root
Debian runtime (uid 10001) with the UI baked in.
# from the repo root
docker build -t <registry>/verdigris:0.0.1 .
docker push <registry>/verdigris:0.0.1
The ingest / query role split¶
The binary takes serve --role {all,ingest,query}. For an S3 backend the chart splits the
write and read surfaces so the query tier scales without ever corrupting writes:
| Role | What it serves | Replicas |
|---|---|---|
ingest |
only the write endpoints (/v1/ingest, /v1/otlp/logs) — the single manifest writer |
1 |
query |
read/UI endpoints + /config.json + static UI; write endpoints answer 405 |
replicaCount (scale freely) |
all |
everything, one process (local demo, or S3 without a dedicated ingest tier) | 1 |
Why the split: multiple writers would race on the JSON manifest. At the data layer this is
already made safe — data files are content-addressed and the manifest commits via optimistic
compare-and-swap with retry — so the single-writer ingest role is defense-in-depth, and
replicaCount scaling the query tier is always safe. (Full Apache Iceberg commits, for
scale/partitioning rather than correctness, remain future work.)
Install: zero-config local demo (no AWS)¶
A filesystem backend on a PVC, auto-seeded with synthetic logs — a queryable UI out of the box, useful to smoke-test the chart before wiring S3:
helm install vdg deploy/helm/verdigris \
--set image.repository=<registry>/verdigris --set image.tag=0.0.1
kubectl port-forward svc/vdg-verdigris 8080:8080
# open http://localhost:8080
This is always a single --role all pod — a pod's local filesystem isn't shared across
replicas.
Install: production on EKS + S3¶
Point the chart at your bucket and attach an IRSA role. Preferred auth is IRSA, so no static keys live in the cluster:
helm install vdg deploy/helm/verdigris \
--set image.repository=<registry>/verdigris --set image.tag=0.0.1 \
--set storage.backend=s3 \
--set storage.s3.bucket=my-company-logs \
--set storage.s3.region=us-east-1 \
--set replicaCount=3 \
--set-string serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::<acct>:role/verdigris-s3
Credentials resolve through the standard AWS chain (AmazonS3Builder::from_env), so an
IRSA web-identity "just works" with no keys in the chart.
IAM permissions¶
The IRSA role needs, on the bucket:
s3:GetObject,s3:PutObject,s3:DeleteObject,s3:ListBucket
If you let the chart auto-apply the lifecycle policy (below), the role also needs
s3:PutLifecycleConfiguration.
MinIO or static keys¶
For MinIO or an environment without IRSA, put AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
in a Secret and reference it (avoid inline plaintext keys):
--set storage.s3.endpoint=http://minio:9000 --set storage.s3.allowHttp=true \
--set storage.s3.existingSecret=verdigris-s3-creds
S3 lifecycle policy (auto-applied)¶
Tiering rules (hot → warm → cold → expire) are an S3-API concern, so the binary owns them.
object_store has no lifecycle API, so this path uses the AWS SDK behind the optional
apply feature (already built into the image). On an s3 install/upgrade the chart runs a
hook Job that calls vdg lifecycle --table logs --apply, pushing the policy via
PutBucketLifecycleConfiguration with the pod's IRSA creds — so a fresh install actually
tiers, with no manual step. The age thresholds come from the lifecycle.* values.
Disable it (to manage the policy out-of-band) with --set lifecycle.autoApply=false, then
apply it yourself:
kubectl exec deploy/vdg-verdigris -- vdg lifecycle --table logs # print the policy JSON
kubectl exec deploy/vdg-verdigris -- vdg lifecycle --table logs --apply # or apply it directly
Values you actually set¶
| Value | Default | Purpose |
|---|---|---|
storage.backend |
local |
s3 for production |
storage.s3.bucket |
"" |
required for s3 |
storage.s3.region |
us-east-1 |
bucket region |
storage.s3.prefix |
"" |
optional key prefix within the bucket |
storage.s3.endpoint / allowHttp |
"" / false |
set for MinIO |
replicaCount |
1 |
scales the query tier (s3 + dedicated ingest) |
ingest.dedicated |
true |
separate single-writer ingest Deployment (s3) |
serviceAccount.annotations |
{} |
attach the IRSA role-arn here |
query.cores / query.modeledMibpsPerCore |
4 / 250 |
the query compute dial |
routing.{error,warn,info,debug} |
hot/warm/warm/cold | severity → tier placement |
lifecycle.{hotToWarmDays,warmToColdDays,expireDays} |
3 / 30 / 400 |
age transitions |
lifecycle.autoApply |
true |
push the lifecycle policy on install/upgrade (s3) |
seed.enabled / seed.records |
true / 20000 |
one-shot synthetic seed |
vector.enabled |
false |
opt-in Vector log-shipping DaemonSet |
Ship real logs¶
The chart includes an opt-in Vector DaemonSet that tails every pod's stdout/stderr and ships NDJSON to the single writer:
Its sink defaults to the dedicated ingest Service when one exists, so writes always land on the single writer, never on the scalable query replicas. See Sending logs for Vector, Fluent Bit, and OTLP details.
Health checks¶
GET /healthz returns 200 {"status":"ok"} in every role and is never gated by auth,
so kubelet probes can target it. The ingest role serves no web root, so point its probes at
/healthz rather than /.
Validate the chart without a cluster¶
helm lint deploy/helm/verdigris
helm template vdg deploy/helm/verdigris # local backend
helm template vdg deploy/helm/verdigris --set storage.backend=s3 --set storage.s3.bucket=x
Locking down the API¶
The /v1/* surface can be gated with an optional bearer token — off by default. See
Configuration → Authentication. Note that the
live-tail SSE endpoint is consumed by the browser's EventSource, which can't send an
Authorization header; front it with a query-param token or ingress auth when [auth] is on.