Sign inSign up

beztebya666/etcd-ui

By beztebya666

Updated 4 months ago

Universal, fast, beautiful UI for any etcd cluster.

Image
API management
Databases & storage
Monitoring & observability
1

388

beztebya666/etcd-ui repository overview

etcd-ui

Universal, fast, beautiful UI for any etcd cluster — Kubernetes, Patroni, standalone or DIY. Ships as a single Docker image with a microservice backend.

Demo

K8s edit round-trip: open a Pod under /registry/pods/... → server decodes the protobuf through k8s.io/api into structured JSON → click Edit, change the container image to v9.9.9-DEMODone editingSave → server validates the edited JSON against the typed Go struct (catches field-name typos), re-marshals to protobuf, wraps in runtime.Unknown, writes to etcd under a CAS guard. The whole flow is ~14 seconds.

K8s edit demo

Screenshots

Real screenshots against a live etcd 3.5.15 seeded with realistic Kubernetes objects (Pods, Deployments, Services, ConfigMaps, Jobs). The capture script lives at scripts/screenshots/capture.mjs — Playwright + Chromium, idempotent, points at any running etcd-ui.

Dashboard — every cluster you've connected with live health, leader, db size, key count, revision sparklineDashboard
Browser tree + Pod fully decodedkv links k8s.io/api, so the binary protobuf in /registry/pods/... round-trips into pretty JSON in the editor with proper Prism syntax highlighting. Edit it, hit Save — server re-encodes back to protobuf under a CAS guard.Pod decoded
Cluster — members, raft term, leader, db size, key count + a Make leader button on every follower with a confirmation modal (routes to the current leader endpoint automatically)Cluster
Live watch — every PUT/DELETE under a prefix in real time, with per-event K8s preview chips, filter pills, expandable raw bodyWatch
Metrics — Grafana-style chart hover with vertical guide + value-at-cursor; leader-change log with from → to member IDs and a "why we can't tell the cause" explainerMetrics
Heatmap — write activity aggregated by prefix bucket, liveHeatmap
Distributed locks — acquire and queue lease-bound locks under a prefix; same primitive clientv3/concurrency.Mutex uses; open two tabs to watch fair queuingLocks
Maintenance — snapshot, restore, compact, defrag, etcdutl snapshot validation, K8s-enriched leases with holder identity + renew timeMaintenance
Audit — every mutation through the gateway, JSONL-persisted, SSE-tailableAudit
Permissions — matrix view + inline editor with three-column diff preview before commit; federation principals (system:peer:*, peer:*/*) get a globe badge and a "federation only" filterPermissions
Federation hub — aggregate remote etcd-ui peers, each card surfaces the ACL rules governing that peer at this hub + a "manage" deep-link into the matrix scoped to that peerFederation
Settings — cluster registry editor, themes, language, version footerSettings

Why

Existing etcd UIs are either tied to one platform (kubectl, Patroni admin) or look like 2014. This is one polished UI for any etcd, regardless of where it lives. Connect once — get cluster health, KV browser, live watch streams, snapshots, lease management, members, alarms, RBAC, audit log and a command palette to do anything in two keystrokes.

Architecture

Single Docker image, five Go microservices behind a tiny supervisor (pid 1). Each owns its concerns and can be split into its own pod tomorrow.

flowchart LR
    browser([Browser]) -->|HTTPS · WebSocket · SSE :8080| gw

    subgraph image["single Docker image (+ etcdctl + etcdutl)"]
        sup["supervisor<br/>(PID 1)"] -. supervises .-> gw & cluster & kv & ops & audit
        gw["gateway :8080<br/>SPA + REST + WS/SSE multiplex<br/>audit-emit · OIDC / Basic / Session<br/>federation hub fan-out"]
        gw -->|HTTP/JSON · WS over 127.0.0.1| cluster
        gw --> kv
        gw --> ops
        gw --> audit

        cluster["cluster :7001<br/>discovery (k8s/Patroni/SRV/file)<br/>conn pool · health · members<br/>alert manager · /alerts/log"]
        kv["kv :7002<br/>range/put/delete/bulk<br/>txn · watch (WS+SSE) · history · diff<br/>put-cas (3-way merge)<br/>k8sproto + full k8s.io/api decode"]
        ops["ops :7003<br/>snapshot · restore · leases (K8s-enriched)<br/>defrag · compact · alarms · RBAC · metrics<br/>etcdctl shell · etcdutl validate<br/>locks playground · move-leader"]
        audit["audit :7004<br/>JSONL ring + SSE tail<br/>compaction (size+age)<br/>ACL history snapshots"]
    end

    cluster -->|gRPC v3 / HTTPv2| etcd[(etcd cluster<br/>k8s · Patroni · Vitess<br/>Vault · standalone · …)]
    kv --> etcd
    ops --> etcd

    gw -. OTLP traces/metrics .-> otel[(OpenTelemetry<br/>collector)]
    gw -. peer→peer .-> peers[("remote etcd-ui<br/>peers (federation)")]
    ops -. SigV4 chunked upload .-> s3[(S3/MinIO/GCS<br/>snapshot offload)]
ASCII fallback if Mermaid isn't rendered:

   ┌──────────┐   pid 1 — restarts crashed services
   │supervisor│
   └────┬─────┘
        ▼
   ┌─────────┐   :8080  HTTPS + WebSocket + SSE + static SPA
   │ gateway │◀── browser     (only port exposed; WS preferred, SSE fallback)
   └────┬────┘
        │ HTTP/JSON · WS over 127.0.0.1
        ├──────────────┬───────────────────┬──────────────┐
        ▼              ▼                   ▼              ▼
     cluster:7001  kv:7002             ops:7003       audit:7004
     discovery     range/put/delete    snapshot/restore  JSONL ring
     pool, health  bulk, txn, watch    leases (k8s)      SSE tail
     /alerts/log   put-cas (CRDT)      compact/defrag    ACL history
                   k8s.io/api decode   locks, move-leader
                   history, diff       etcdctl, etcdutl
                       │
                       ▼
              etcd cluster(s)   gRPC v3 — auto-detected vs v2

   gateway → S3/MinIO/GCS (snapshot offload, SigV4 chunked)
   gateway ⇄ remote etcd-ui peers (federation hub mode)
   gateway → OpenTelemetry collector (OTLP traces + metrics)
Services
ServicePortResponsibility
supervisorBoots & supervises the others; PID 1; forwards signals; restart on crash
gateway8080SPA + REST + WS/SSE multiplex; fans out to internals; emits audit events; OIDC/Basic/Session; federation hub fan-out
cluster7001Auto-discovery (12 sources), connection pool, member health, alert manager, persisted /alerts/log
kv7002Range/Put/Delete, bulk, txn, watch (WS+SSE), put-cas (3-way merge), put-k8s (typed edit round-trip), history, diff, K8s protobuf decode+encode via k8s.io/api (519 Kinds), /range/counts for tree truncation hints
ops7003Snapshot, JSON restore, leases (K8s-enriched holder identity + renew time), compact, defrag, alarms, RBAC, metrics, etcdctl shell, etcdutl snapshot validate, locks playground, move-leader (auto-routes to current leader endpoint)
audit7004Append-only JSONL log + in-memory ring + SSE live tail + ACL history snapshots
Auto-discovery

etcd is just a key-value store, and lots of systems use it. etcd-ui doesn't care which one — it finds them all.

Five discovery sources run in parallel; each is opt-in via env, all coexist:

SourceHow
envETCD_ENDPOINTS=https://etcd:2379 for a single cluster
fileCLUSTERS_FILE=/etc/etcd-ui/clusters.yaml, many clusters, hot-reloaded every 30 s
dns-srvETCD_UI_DNS_SRV="prod=Prod=_etcd-client._tcp.prod.example.com,https;…"
patroniPATRONI_URLS=http://pg-1:8008,… — pulls DCS info from Patroni REST
kubernetesMultiple label selectors out of the box: control-plane, Vitess, Cilium, KubeEdge, Karmada, APISIX, M3DB, generic app=etcd, plus your own via ETCD_UI_K8S_SOURCES JSON

For step-by-step "how do I get etcd endpoints from <my system>?" instructions covering Kubernetes (stacked / external / managed), Patroni, Vitess, HashiCorp Vault, Apache APISIX, Cilium, KubeEdge, Karmada, Talos Linux, OpenStack tooz, M3DB, CoreDNS, Calico, SkyDNS — see docs/CONNECT.md.

The UI also has a built-in Add Cluster wizard (Settings → Add cluster…) with presets and concrete commands inline for each of these systems.

Once discovered, every cluster looks identical to the UI.

Features

Core
  • 🌳 Key tree with multi-select, drag-import, live refresh, JSON formatter
  • 📡 Live watch streams (Server-Sent Events) with pause / resume / clear
  • 📊 Multi-cluster dashboard with health, leader, db size, alarms — auto-refresh every 3 s
  • 🧭 Cluster details — members, leader, raft term, learner status
  • 🔧 Maintenance — snapshot (.db), JSON export, defrag, compact, alarm disarm, lease management
Power-user
  • 🧮 Transaction builder — visual if / then / else over etcd Txn; supports compare on value / createRev / modRev / version
  • 📦 Bulk operations — multi-select keys, bulk delete, bulk export, drag-and-drop bulk import
  • 🔄 Restore — upload a .json export back to the cluster, with optional "wipe prefix before import"
  • ⌨️ Command palette (⌘K) — jump between pages, switch clusters, change theme, download snapshot, etc.
  • 🎨 Themes — dark / light / system, persisted
Security & audit
  • 👥 etcd RBAC editor — enable auth, manage users, roles, permissions (read / write / readwrite, prefix-aware)
  • 🛡️ etcd-ui ACL — per-cluster-per-user (and per-prefix) access control inside the UI itself. Hot-reloaded from a JSON file. See docs/PERMISSIONS.md.
  • 🔐 SSO — full OIDC authorization-code + PKCE login flow with refresh-token rotation. Falls back to HTTP Basic if no IdP is wired.
  • 📝 Audit log — every write done via the UI is recorded as JSONL on disk, visible live with filtering. Hourly compaction by size + age. Per-key Restore button for single + bulk deletes.
  • 🔔 Health alerts — webhook (Slack/Discord/Teams) + browser notifications on cluster degradation; per-(cluster,kind,recipient) throttling.
Operations
  • 💾 Scheduled snapshots — cron-style per-cluster schedules with rotation. Optional offload to S3 / MinIO / GCS / R2 / Wasabi (single PUT up to 5 GiB, multipart above).
  • 🌐 Federation — one hub UI aggregates clusters from remote etcd-ui peers (with OIDC service-account between peers).
  • 📈 Metrics & forecasting — per-node Prometheus scrape, DB-size linear-regression forecast against the 2 GiB quota.
  • 🧪 OpenTelemetry — traces (W3C TraceContext through every internal hop) and metrics (counters/gauges exported as OTel observables).
  • 💻 Terminal — real etcdctl baked into the image with an allow-list (member list, move-leader, endpoint hashkv, …).
  • 📦 Offline etcdutl — also bundled. Validate a .db snapshot before halting your quorum: upload, get back hash/revision/total-keys/size, decide whether the restore is safe (Maintenance → "Validate snapshot").
  • 🔒 Distributed-lock playground/locks page mints lease-bound keys under a chosen prefix, exactly like clientv3/concurrency.Mutex. Watch holder + waiters live, force-release on demand, debug leader-election scenarios without writing a Go program.
  • 👑 Move-leader in one click — Cluster page surfaces a ↑ Make leader button on every follower, with a confirm modal. Routes the call to the current leader endpoint automatically so a 3-node cluster doesn't return "not leader" half the time.
  • 🧬 Full K8s decode + editkv links k8s.io/api (519 Kinds across 19 groups). Pod / Service / Deployment / ConfigMap / Secret / etc. round-trip through their generated Go structs into pretty JSON — no more hex dumps for the bulk of your registry. Edit the structured JSON in place, hit Save: server validates the edit against the Go type (DisallowUnknownFields catches spec.imag vs spec.image typos before they reach etcd), re-marshals to protobuf, wraps in runtime.Unknown, writes under a CAS guard. Round-trip is provably lossless — same bits in, same bits out. CRDs stored as JSON are pretty-printed and edited directly; CRDs stored as proto without a typed shim fall back to metadata-only preview + raw hex.
  • 🌐 API protocol badge — each cluster card shows whether the gateway speaks gRPC (v3) or HTTP (v2) to it, with the etcd server version next to it. Auto-detected from /version on first contact.
Friendly
  • Onboarding tour — 9-step walk-through; replayable from Settings or the command palette
  • 🙂 Simple mode toggle — friendlier labels for first-time users
  • A11y first — focus traps on every modal, full keyboard nav in the tree (↑↓/←→/Enter/Space), prefers-reduced-motion honoured
  • 💡 Generous tooltips, kbd shortcut hints, zero modal dialogs that block more than one action

Run it

# from-source
docker build -t etcd-ui:dev .
docker run --rm -p 8080:8080 -e ETCD_ENDPOINTS=http://your-etcd:2379 etcd-ui:dev

Open http://localhost:8080.

mTLS
docker run --rm -p 8080:8080 \
  -e ETCD_ENDPOINTS=https://etcd:2379 \
  -e ETCD_CA_FILE=/certs/ca.pem \
  -e ETCD_CERT_FILE=/certs/client.pem \
  -e ETCD_KEY_FILE=/certs/client-key.pem \
  -v /path/to/certs:/certs:ro \
  -v etcd-ui-data:/app/data \
  etcd-ui:dev

The /app/data volume persists the audit log across restarts.

Kubernetes (Helm)

Minimal — in-cluster discovery, ephemeral state:

helm install etcd-ui ./deploy/helm/etcd-ui \
  --namespace kube-system \
  --set discovery.kubernetes=true

Production — discovery, persistence, auth, scheduled snapshots, monitoring:

# 1) create the auth secret (bcrypt: htpasswd -nbB admin password)
kubectl create namespace etcd-ui
kubectl -n etcd-ui create secret generic etcd-ui-auth \
  --from-literal=AUTH_USERS='admin:bcrypt:$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  --from-literal=AUTH_SESSION_SECRET="$(openssl rand -base64 48)"

# 2) install
helm install etcd-ui ./deploy/helm/etcd-ui -n etcd-ui \
  --set discovery.kubernetes=true \
  --set persistence.enabled=true \
  --set persistence.size=10Gi \
  --set auth.existingSecretName=etcd-ui-auth \
  --set snapshotSchedule="kube-system:hourly:24" \
  --set hardening.corsOrigins="https://etcd-ui.example.com" \
  --set metrics.serviceMonitor.enabled=true \
  --set ingress.enabled=true \
  --set ingress.host=etcd-ui.example.com \
  --set ingress.tls=true \
  --set ingress.tlsSecretName=etcd-ui-tls

All knobs live in deploy/helm/etcd-ui/values.yaml. Quick map:

BlockWhat it controls
clustersStatic profiles. One entry → env vars; many → ConfigMap with clusters.yaml mounted at /etc/etcd-ui/ and read by the file discovery source.
discovery.*k8s pod selectors (kubernetesSources JSON), Patroni REST URLs, DNS SRV records.
auth.* / oidc.*Basic users + signed session secret; OIDC issuer / audience / username claim. Set oidc.clientID + redirectURL to enable the full PKCE login flow with refresh-token rotation.
acl.*Per-cluster-per-user RBAC. Inline rules or file/ConfigMap (hot-reloaded via inotify on Linux, mtime-poll elsewhere). __acl__ synthetic cluster gates the editor; bootstrapAdmin seeds it. Edits flow through PUT /api/acl and land in the audit log with a JSON diff.
alerts.*Slack/Discord/Teams/MS-Teams webhook URLs. Per-(cluster, kind, recipient) throttling — each webhook URL has its own bucket.
audit.*JSONL retention: hourly compaction by maxBytes (default 256 MiB) and maxAge (default 720h).
snapshotOffload.*S3 / MinIO / GCS / R2 / Wasabi / B2. < 4.5 GB → single PUT; ≥ 4.5 GB → multipart with 4× parallel workers; streaming SigV4 path for non-seekable sources.
federation.*Hub aggregates clusters from remote peers. Peer→peer auth via shared token or OIDC service-account. peerClientIDs lets a peer recognise federation hops as system:peer:<id>; the hub propagates the calling human via X-Etcd-UI-On-Behalf so peer ACL can apply real per-user rules.
gatewayTLS.*Native HTTPS listener — mount a Secret with tls.crt + tls.key.
hardening.*CORS allowlist, per-user rate limit, custom CSP, pprof toggle.
readonlyClustersCluster IDs that kv/ops refuse to mutate (cluster-wide block, complements acl.*).
snapshotScheduledefault:hourly:24,prod:daily:30 etc. Files land in /app/data/snapshots/<cluster>/.
persistence.*PVC for /app/data (audit log + UI clusters + snapshots).
networkPolicy.*Ingress source allowlist; egress permits DNS, k8s API, etcd ports.
metrics.serviceMonitor.*Prometheus Operator ServiceMonitor.
pdb.*PodDisruptionBudget — only created when replicaCount > 1.
containerSecurityContextnon-root, drop all caps, no privilege escalation by default.
OpenTelemetry

Both traces and metrics are exported via OTLP/HTTP when an endpoint is set:

OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://otel-collector:4318     # optional, separate pipeline
OTEL_TRACES_SAMPLER_ARG=0.1                                        # 10% sampling in high-traffic clusters

traceparent propagates across all internal hops (peer.Syncer, audit-emit, metrics-scrape, federation, OIDC). Inside handlers each cli.Get/Put/Delete/Txn is wrapped in a child etcd.* span so the trace tree shows the actual etcd round-trip.

Streaming & live transport

Watch streams + audit + alerts ride a WebSocket-first, SSE-fallback transport. The pill in the top-right corner shows WS · N (green), SSE · N (proxy) (amber, means a reverse proxy is buffering and rejecting the upgrade), or reconnect in 4s (clock) with a per-second countdown.

WebSockets carry bearer tokens via Sec-WebSocket-Protocol: etcd-ui.bearer, <jwt>, etcd-ui.v1 — works in Safari ITP / embedded webviews where cookies are dropped from the upgrade handshake. Client also sends a {"type":"ping"} text frame every 25s to keep enterprise proxies from idle-killing the connection; the server reads + responds with a Pong.

Settings → Live transport panel exposes a rolling 200-event ring of every WS/SSE open/close/reconnect/fallback (persisted to localStorage). "Copy diagnostic JSON" attaches the full trace to a bug report.

Permissions editor

Edit __acl__ rules straight from the SPA — /permissions has matrix view + inline editor with three-column diff preview before commit. Every save snapshots before/after to $ETCD_UI_DATA_DIR/acl-history/; the History drawer lets you restore any prior snapshot (Undo). Audit log rows for action=acl.edit get a Snapshot button that deep-links into that drawer.

Server validates the proposed ruleset (POST /api/acl/validate) before any commit so typos in access values or malformed prefixes surface before the diff dialog.

Federation hub

/federation lists every remote etcd-ui peer with per-peer health (healthy / degraded / down / empty), reachable flag, last-checked timestamp and a foldable cluster roster. Cluster IDs are namespaced <peer>/<cluster> so they don't collide with locally-defined ones.

Each peer card also shows the policy at this hub — how many ACL rules govern that peer's traffic, with a manage deep-link into /permissions?peer=<id> that filters the matrix to just that peer's system:peer:<id> and peer:<id>/<user> rules. The Permissions matrix renders federation principals with a globe badge and a "federation principals only" toggle, so cross-hub access is auditable in isolation from local users.

Browser concurrent-edit handling (CRDT-lite, 3-way merge)

Two affordances kick in when somebody else writes to a key you're editing:

  1. Live banner — amber warning above the editor ("base rev N → current rev M — Save will overwrite") with a one-click Reload. modRevision-based, zero overhead, no merge yet.
  2. CAS + line-based 3-way mergeSave doesn't blindly PUT. It hits POST /api/clusters/{id}/put-cas which:
    • Snapshots baseRev (the revision you opened),
    • Runs a Txn(If ModRevision(key) == baseRev, Then Put) for the no-contention path,
    • Falls back to a line-level diff3 weave (internal/threewaymerge, zero deps) when ours and theirs both diverged from base. Non-overlapping line edits merge cleanly; overlapping ones produce a 3-pane resolver dialog (Base / Yours / Theirs + per-block "use ours / use theirs / both" buttons).
    • History compaction is tolerated — if baseRev was compacted away, the live value is treated as base.

See docs/features/CRDT.md for the full algorithm.

Sessions & SSO

OIDC supports both modes:

  • Bearer-only (auth-proxy already mints a token): set oidc.issuer / audience.
  • Full login flow (PKCE): also set oidc.clientID + redirectURL. Refresh tokens are stored in an HttpOnly cookie and rotated on every /api/auth/oidc/refresh. The SPA refreshes proactively 60s before session expiry; on cookie revocation it falls through to a prompt=none iframe (silent re-auth) before bouncing to /login.

Single-file manifest is in deploy/k8s.yaml for clusters without Helm.

Develop

make dev          # backend (4 services) + Vite dev server with proxy
make build        # build all 5 Go binaries into ./bin
make build-image  # docker build -t etcd-ui:dev .
make test         # go test ./... + (cd web && npm test)

Backend on :8080, Vite dev on :5173 (proxied to backend for /api).

Tag summary

Content type

Image

Digest

sha256:afe55f7e6

Size

53.3 MB

Last updated

4 months ago

docker pull beztebya666/etcd-ui