https://github.com/sergeyslonimsky/elara
Kube-native configuration management with a Web UI and an etcd-compatible gRPC API
https://github.com/sergeyslonimsky/elara
config-server configuration-management etcd go golang grpc helm-chart kubernetes self-hosted web-ui
Last synced: 2 months ago
JSON representation
Kube-native configuration management with a Web UI and an etcd-compatible gRPC API
- Host: GitHub
- URL: https://github.com/sergeyslonimsky/elara
- Owner: sergeyslonimsky
- License: mit
- Created: 2026-04-18T00:07:30.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2026-04-18T03:08:04.000Z (3 months ago)
- Last Synced: 2026-04-18T03:13:41.620Z (3 months ago)
- Topics: config-server, configuration-management, etcd, go, golang, grpc, helm-chart, kubernetes, self-hosted, web-ui
- Language: TypeScript
- Homepage: https://sergeyslonimsky.github.io/elara
- Size: 1.26 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 10
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
Elara
Configuration management service with a Web UI, a ConnectRPC API, and an etcd-compatible gRPC API.
Elara stores, edits, and serves application configuration. Operators use the
built-in Web UI for CRUD; services consume values through the same API
surface as etcd (drop-in for any etcd v3 client) or through a typed
ConnectRPC client. A single bbolt file holds all state with ACID
transactions and global revision tracking.
**Status:** early, pre-1.0. Single-instance bbolt today; raft-based HA and
pluggable storage backends (PostgreSQL, S3) are on the roadmap.

## Features
- **Web UI** for browsing, creating, editing, and deleting configs across namespaces.
- **ConnectRPC API** (`elara.config.v1.ConfigService`, `elara.namespace.v1.NamespaceService`, …) — works from Go, TypeScript, Python, etc. with native clients.
- **etcd-compatible gRPC API** on port 2379 (`KV`, `Watch`, `Maintenance`, `Cluster`) — connect with `etcdctl` or any etcd v3 SDK.
- **Config history** — every version stored, retrievable by revision.
- **Global revision counter** — monotonic, etcd-style semantics.
- **Format-aware validation** for JSON and YAML; pass-through for everything else (ini, toml, plain text).
- **Single bbolt file storage** — ACID transactions, no external DB required.
- **Observability** — optional Prometheus `/metrics` and OTLP tracing.
- **Kube-native Helm chart** with StatefulSet, ServiceMonitor, NetworkPolicy, JSON Schema validation, and a smoke test.
## Quick start
Run locally with Docker:
```bash
docker run --rm -p 8080:8080 -p 2379:2379 ghcr.io/sergeyslonimsky/elara:latest
```
Open for the UI. Talk to the etcd-compatible API at
`localhost:2379`. Jump to [Deploying to Kubernetes](#deploying-to-kubernetes)
for the Helm path.
## Usage
### Web UI
The UI (served on the HTTP port, port 8080 by default) covers the full
operator workflow:
- **Dashboard** — cluster-wide KPIs (total namespaces, configs, active
clients, current global revision) plus the last 20 config changes and a
per-namespace config count.
- **Configs** — directory-style browser across folders/files, per-namespace.
Create, edit (format-aware: JSON / YAML / raw), copy, delete, and view
version history. Every edit bumps the global revision.
- **Namespaces** — CRUD for namespaces (logical grouping of configs).
Deletion is blocked while the namespace still has configs.
- **Clients** — live list of connected etcd-compatible clients, with
recent events and basic history.
### etcd-compatible CLI
Any etcd v3 client works. Example with `etcdctl`:
```bash
export ETCDCTL_API=3
export ETCDCTL_ENDPOINTS=localhost:2379
# Write a config (namespace = prefix segment, path = key)
etcdctl put /prod/services/billing/config.yaml "$(cat config.yaml)"
# Read it back
etcdctl get /prod/services/billing/config.yaml
# Watch a prefix for live updates
etcdctl watch --prefix /prod/services/billing/
# Check endpoint health
etcdctl endpoint health
```
### Locked configs and namespaces
Lock is an admin/ops concern, not a data concern. Configs and namespaces can each be locked
independently from the Web UI or the v2 connectrpc admin API; effective lock is true if either
is locked. Locking protects against accidental writes — it does not affect the data itself.
For etcd clients the contract is intentionally narrow:
- **Reads and watches always work.** Locked configs are returned by `Get`/`Range` and appear in
`Watch` like any other key. The lock state is not surfaced to etcd clients.
- **Writes on locked targets fail with `FailedPrecondition`.** Both `Put` and `DeleteRange` return
the same uniform message regardless of cause:
```text
Error: etcdserver: put: config "/services/billing/config.yaml" is locked
```
etcd has no concept of namespace, so the message always reads as if the *config* is locked
even when the parent namespace is the actual cause.
- **Watch streams do not carry lock/unlock events.** This is by design — the etcd channel is a
clean data plane. Subscribe to `WatchConfigs` on the v2 connectrpc API if you need to react
to lock state changes.
- **Lock management** (lock/unlock, audit history) is available only via the Web UI and the v2
connectrpc admin API. There is no etcd-side knob to flip the lock.
Operators can detect clients that keep retrying against locked targets via the
`elara_writes_rejected_total{op,reason,namespace}` Prometheus counter, where `reason` is
`config_locked` or `namespace_locked`.
### ConnectRPC client (Go)
```go
import (
"connectrpc.com/connect"
"net/http"
configv1 "github.com/sergeyslonimsky/elara/gen/elara/config/v1"
"github.com/sergeyslonimsky/elara/gen/elara/config/v1/configv1connect"
)
client := configv1connect.NewConfigServiceClient(
http.DefaultClient,
"http://localhost:8080",
)
resp, _ := client.CreateConfig(ctx, connect.NewRequest(&configv1.CreateConfigRequest{
Namespace: "default",
Path: "/services/billing/config.yaml",
Content: []byte("retries: 3\n"),
}))
```
### ConnectRPC client (TypeScript)
```ts
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { ConfigService } from "./gen/elara/config/v1/config_service_pb";
const client = createClient(
ConfigService,
createConnectTransport({ baseUrl: "http://localhost:8080" }),
);
await client.createConfig({
namespace: "default",
path: "/services/billing/config.yaml",
content: new TextEncoder().encode("retries: 3\n"),
});
```
## Deploying to Kubernetes
The chart lives at [`helm/elara/`](helm/elara/) and is designed to be
production-ready by default: StatefulSet with `volumeClaimTemplates`,
non-root security context, JSON-Schema-validated values, optional
ServiceMonitor and NetworkPolicy, and a `helm test` smoke check.
### Install from the Helm repository
Once the GitHub Pages repo is published:
```bash
helm repo add elara https://sergeyslonimsky.github.io/elara
helm repo update
# default: single replica, 2Gi RWO PVC, ClusterIP service
helm install elara elara/elara --namespace elara --create-namespace
```
### Install from a checkout
```bash
helm install elara ./helm/elara --namespace elara --create-namespace
```
### Production values
```yaml
# values-prod.yaml
image:
digest: sha256:… # pin by digest, not tag, in prod
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { cpu: "2", memory: 1Gi }
persistence:
size: 50Gi
storageClassName: ssd
ingress:
enabled: true
className: nginx
hosts:
- host: elara.example.com
paths: [ { path: /, pathType: Prefix, port: http } ]
tls:
- secretName: elara-tls
hosts: [ elara.example.com ]
metrics:
enabled: true
serviceMonitor:
enabled: true
labels: { release: kube-prometheus-stack }
tracing:
enabled: true
otlpEndpoint: http://otel-collector.observability:4318
```
```bash
helm install elara elara/elara -f values-prod.yaml \
--namespace elara --create-namespace
```
### Upgrade
```bash
helm upgrade elara elara/elara --namespace elara -f values-prod.yaml
```
Pods restart automatically on ConfigMap changes (via checksum annotation).
`helm.sh/resource-policy: keep` is NOT applied to the PVC, but because the
chart uses `volumeClaimTemplates`, `helm uninstall` leaves the PVC in
place regardless — data survives uninstall.
### Uninstall
```bash
helm uninstall elara --namespace elara
# Optional: drop the PVC too (destroys all stored configs)
kubectl delete pvc data-elara-0 --namespace elara
```
### Exposing the etcd-compatible gRPC port
The chart Ingress exposes only the HTTP / ConnectRPC / UI port (8080).
Port 2379 (etcd gRPC) is reachable cluster-internally over the ClusterIP
service by default. For external exposure, use `service.type: LoadBalancer`
or add a gRPC-aware Ingress — see the [chart
README](helm/elara/README.md#grpc-exposure).
### Invariants
- `replicaCount` is schema-pinned to `1` until raft-based HA is implemented.
bbolt holds an exclusive file lock — more than one replica corrupts data.
The schema will relax to `minimum: 1` when raft ships.
- `persistence.accessMode` is pinned to `ReadWriteOnce` for the same reason.
- `storage.type` currently accepts only `bbolt`; the enum will expand with
future storage backends.
The full values reference, extensibility hooks, and examples live in
[`helm/elara/README.md`](helm/elara/README.md).
## Local development
```bash
make proto # regenerate protobuf stubs
make test # go test -race ./...
make lint # golangci-lint
make format # golines + gofumpt + gci
go run ./cmd/service
```
The UI is served embedded from `web/dist`; for live reload during frontend
work run `cd web && npm run dev` and hit .
## Architecture
```
Web UI (React) ──┐
ConnectRPC client ┤──→ HTTP/2 server (:8080) ──→ UseCases ──→ Domain
etcdctl / grpc ──────→ gRPC server (:2379) ──→ UseCases ──→ Domain
│
Adapter ────┘
(bbolt)
```
- **Handler** — ConnectRPC / etcd gRPC; proto ↔ domain conversion.
- **UseCase** — application logic; each usecase owns its minimal interface.
- **Domain** — pure entities, validation, errors; no infrastructure imports.
- **Adapter** — bbolt storage and in-memory watch pub/sub.
## Configuration
All config keys flow through Viper; environment variables override every
source. See the
[mapping table in the chart README](helm/elara/README.md#how-configuration-reaches-the-service)
for the full list.
Key defaults:
| Key | Env var | Default |
| --------------------- | ---------------------- | ---------------- |
| `http.frontend.port` | `HTTP_FRONTEND_PORT` | `8080` |
| `grpc.etcd.port` | `GRPC_ETCD_PORT` | `2379` |
| `config.data.path` | `CONFIG_DATA_PATH` | `./data` |
| `metrics.enabled` | `METRICS_ENABLED` | `false` |
| `tracing.enabled` | `TRACING_ENABLED` | `false` |
| `log.level` | `LOG_LEVEL` | `info` |
| `log.format` | `LOG_FORMAT` | `json` |
| `log.noSource` | `LOG_NOSOURCE` | `false` |
## Contributing
PRs welcome. A few house rules:
- Go: `golines` (120 cols), `gofumpt`, `gci` (stdlib → default → `github.com/sergeyslonimsky/elara` prefix).
- Proto: `make proto` — `buf lint` and `buf breaking` run in CI.
- Tests: `go test -race` must pass.
- Keep changes focused; split unrelated refactors into separate PRs.
## License
[MIT](LICENSE).