Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Platform Substrate

The platform substrate is the set of Kubernetes primitives that easy-db-lab provisions on every cluster. It provides a stable foundation so any kit can be deployed without bespoke manifest code.

Two-Layer Model

┌─────────────────────────────────────┐
│         Kit Layer              │
│  (ClickHouse, Presto, custom, …)    │
│  deployed via install + helm/kubectl│
└────────────────┬────────────────────┘
                 │ uses
┌────────────────▼────────────────────┐
│        Platform Substrate           │
│  StorageClasses · Node Labels · PVs │
│  provisioned at cluster `up` time   │
└─────────────────────────────────────┘

The platform substrate is provisioned automatically when you run easy-db-lab up. You do not need to configure it manually.

StorageClasses

Two StorageClasses are created at up time:

NameBinding ModeReclaim PolicyUse
local-storageImmediateRetainLegacy / direct-bound PVs
local-storage-wfcWaitForFirstConsumerDeletePer-kit PVs via install / platform create-pvs

local-storage-wfc is used for StatefulSet kits. Kubernetes must know which node a pod schedules on before binding the volume — WaitForFirstConsumer enforces this ordering.

Node Labels

All cluster nodes are labeled at up time:

LabelValuesApplied to
typedb, app, controlAll nodes
easydblab.com/node-ordinal0, 1, 2, …db and app nodes

Use nodeSelector: type: db (or app) in pod specs to constrain placement. The ordinal label is used by the PV pre-binding mechanism so each StatefulSet replica lands on the right node.

Persistent Volumes

Per-kit PVs are created lazily at install time, not at cluster-up time. Run platform create-pvs before starting a stateful kit:

easy-db-lab platform create-pvs --kit clickhouse --size 100Gi

This creates one PV per db node with:

  • Path: /mnt/db1/<kit> on each host
  • StorageClass: local-storage-wfc
  • Node affinity: type=<node-type> and easydblab.com/node-ordinal=N for deterministic binding. Both pools carry ordinals, so the node type keeps a db PV off the app node with the same ordinal.
  • ClaimRef: pre-bound to <volumeClaimTemplateName>-<kit>-N

The command is safe to re-run. If a PV exists with a stale claimRef (the PVC was deleted), the UID is cleared and the PV is returned to Available.

platform Commands

platform create-pvs

easy-db-lab platform create-pvs --kit <name> --size <Gi> [--node-type db|app] [--pvc-name <name>]

Creates one PV per node of the specified type. Defaults to db nodes. For stateless kits that need app-node storage, use --node-type app. --pvc-name sets the volumeClaimTemplate name to pre-bind against (default: data).

platform info

easy-db-lab platform info

Displays StorageClasses, available PV counts per node pool, node selector labels, and the ordinal label key. Use this to verify substrate readiness before deploying a kit.

platform cni

easy-db-lab platform cni

Shows the pod-network datapath. On a Cilium cluster it reads the cilium-config ConfigMap and the CiliumNode objects from the control node and prints the routing mode, IPAM mode, kube-proxy replacement, masquerade interfaces, native routing CIDR, the Hubble UI URL, and per node the ENI count, subnet CIDRs, and IPs allocated, used, and available. On a Flannel cluster it prints one line that names Flannel. Cilium is the default datapath; init --cni=flannel selects Flannel. Read-only. See Pod Networking (CNI).

Custom Templates

The install command can render templates from a custom directory:

easy-db-lab kit install --from ./my-kit/ --kit my-kit --size 50Gi

The directory must contain a kit.yaml. Without it the install is rejected before anything is written to the workspace, because kit.yaml is what marks an installed directory as a kit.

Template Variable Contract

All templates receive these standard variables from cluster state:

VariableDescription
__CLUSTER_NAME__Cluster name
__CONTROL_HOST__Control node public IP (alias for __CONTROL_HOST_PUBLIC__)
__CONTROL_HOST_PUBLIC__Control node public IP
__CONTROL_HOST_PRIVATE__Control node private IP (use for intra-cluster connectivity)
__DB_NODE_COUNT__Number of database nodes
__APP_NODE_COUNT__Number of app (stress) nodes
__DB_NODE_IPS__Private IPs of database nodes
__APP_NODE_IPS__Private IPs of app nodes
__BUCKET_NAME__Per-cluster S3 bucket prefix
__ACCOUNT_BUCKET__Account-level S3 bucket (survives cluster teardown)
__REGION__AWS region
__VPC_CIDR__VPC CIDR block
__STORAGE_CLASS_WFC__local-storage-wfc
__KIT_NAME__Kit name
__STORAGE_SIZE__Storage size (e.g., 100Gi)
__KUBECONFIG__Path to local kubeconfig
__EASY_DB_LAB_EXEC__Path to the easy-db-lab executable
__RUNNING_KITS__Names of currently running kits
__OPENSEARCH_ENDPOINT__OpenSearch domain endpoint, if provisioned
__PYROSCOPE_URL__Where a kit's Pyroscope agent ships profiles: the control node's Pyroscope, or the external stack on a telemetry-redirect cluster
__TENANT__The cluster's observability tenant; send it as -Dpyroscope.tenant.id (it becomes X-Scope-OrgID)

Unresolved __VAR__ placeholders emit a warning but do not fail the render.

Template Directory Layout

my-kit/
├── kit.yaml
├── README.md.template
├── values.yaml.template
└── bin/
    ├── start.sh.template
    └── stop.sh.template

Files without .template suffix are copied verbatim.

Profile Templates

Place templates in ~/.easy-db-lab/profiles/<profile>/kits/<name>/ to make them discoverable via kit list. Additional template directories can be registered with kit source add. Resolution priority: profile templates override additional sources, which override built-in templates of the same name.

Port Exposure Model

Kits use standard pod networking (not hostNetwork). Client and metrics ports are surfaced on each EC2 instance's network interface in one of two ways:

  • NodePort services — stateful db kits (ClickHouse, TiDB) expose their ports through a NodePort service, remapping native ports into the NodePort range (30000–32767). Example: ClickHouse HTTP 8123 → 30123.
  • hostPort patches — helm-based app kits (Presto, Trino) patch hostPort mappings onto the coordinator pod at start time, keeping the native port (e.g. 8080 → 8080).

Why ports must reach the host

The OTel collector DaemonSet runs with hostNetwork: true so it can scrape host processes and kit metrics endpoints, and so host JVMs such as Cassandra can push OTLP to it at localhost:4318. Built-in kits are scraped by pod discovery: each collector scrapes the kit pods on its own node directly, on the pod IP and container metrics port, so a metrics port does not need to reach the host for the collector (see Kit Observability). The NodePorts and hostPorts that still expose metrics ports are there for manual inspection (curl <node-ip>:<port>/metrics). Client ports do need to reach the host, and a NodePort-range port can never collide with a database listening on its native port on the host.

Port Assignments

KitProtocolNative portNode portExposure
ClickHouseHTTP812330123NodePort
ClickHouseNative TCP900030900NodePort
ClickHouseMySQL wire900430904NodePort
ClickHousePostgreSQL wire900530905NodePort
ClickHousePrometheus936330936pod SD (per-replica); NodePort also exposed
TiDBMySQL (SQL layer)400030400NodePort
TiDBPrometheus (tidb-sql)1008031080pod SD (per-pod); NodePort also exposed
TiDBPrometheus (tikv)20180—pod SD (per-store)
TiDBPrometheus (pd)237932379pod SD (per-pod); NodePort also exposed
TiDBPrometheus (tiflash)823432234pod SD (per-pod); NodePort also exposed
PrestoHTTP (coordinator)80808080hostPort
PrestoPrometheus90909090pod SD; hostPort also exposed
TrinoHTTP (coordinator)80808080hostPort

When adding a new kit, choose ports that do not conflict with any host process or existing kit in the table above. Each kit's ports are declared in its kit.yaml (metrics and endpoints sections).

Kit Observability

Each kit declares its metrics targets in kit.yaml. metrics is a list — kits with multiple components declare one entry per scrape target, each with a unique job name:

metrics:
  - type: scrape     # pod service discovery — each pod scraped directly, per-pod `instance`
    job: tidb-sql
    pod-selector: "app.kubernetes.io/component=tidb,app.kubernetes.io/instance=tidb"
    port: 10080      # container metrics port, not a NodePort
    path: /metrics
  - type: scrape
    job: tikv
    pod-selector: "app.kubernetes.io/component=tikv,app.kubernetes.io/instance=tidb"
    port: 20180
    path: /metrics

If job is omitted, the kit name is used.

Static NodePort vs. pod discovery. By default a scrape target is a static localhost:<port> NodePort — every collector scrapes it and instance is the collector's hostname. When a target sets pod-selector (a comma-separated K8s label selector), the OTel collector instead uses Prometheus pod service discovery (kubernetes_sd_configs, role: pod): each collector scrapes only the matching pods co-located on its own node, and instance becomes the pod name. Use this when a component has multiple pods behind one service and you need per-pod attribution — a NodePort load-balances scrapes across all pods, so a single store/pod cannot be distinguished. TiKV uses this so each of the 3 stores reports under its own instance (e.g. tidb-tikv-0). Every built-in kit uses pod discovery: a static job runs on every collector, so even a single-instance kit behind a NodePort reports one duplicate series per node.

Three modes are supported:

ModeHow metrics reach the OTel collector
scrapeOTel DaemonSet scrapes a Prometheus endpoint via hostPort
java-agentOTel Java agent inside the JVM pushes OTLP to localhost:4317
helm-nativeKit has built-in OTLP support configured via helm values

Metrics Registration Lifecycle

When a kit with type: scrape targets starts successfully, easy-db-lab:

  1. Creates one ConfigMap easydblab-metrics-<job> per scrape target in the default namespace, labeled easydblab.com/workload-metrics=true and easydblab.com/kit=<kit>, containing the job name, port, and path.
  2. Regenerates the OTel collector ConfigMap to include a Prometheus scrape job per target.
  3. Applies the updated OTel ConfigMap so the running collector picks it up.

When the kit stops:

  1. Deletes all of the kit's metrics ConfigMaps via the easydblab.com/kit label selector.
  2. Regenerates and applies the OTel collector ConfigMap without the kit's scrape jobs.

This is fully automatic — no manual OTel configuration is required when starting or stopping kits.

Verifying the Substrate

# Check StorageClasses and PV availability
easy-db-lab platform info

# List all nodes and their labels
kubectl get nodes --show-labels

# List PVs for a kit
kubectl get pv | grep clickhouse