KV namespaces and variables
Practical guide to global, worker, job, and allocation KV — what keys exist, how to set them, and how to use them in templates and hooks.
Maand stores configuration in a KV store (SQLite key_value table). Values are grouped into namespaces. Think in four layers:
| Layer | Who sets it | Typical use |
|---|---|---|
| Global (bucket) | bucket.conf, build |
Settings shared by every job |
| Worker | workers.json, build |
Host capacity, labels, peers |
| Job | manifest, vars.conf, bucket.jobs.conf, build, hooks |
App config, resource limits |
| Allocation | build + deploy | Per (job × worker): certs, peer list |
Read values with maand cat kv, templates (get), or hooks (maand.kv.get).
Write user config via workspace files or hooks — not by editing maand.db.
Persistence and purge: persistence.md · Index: README.md.
Namespace map
GLOBAL
maand/bucket ← build: bucket_id, jobs, activejobs, job port names (active allocations)
vars/bucket ← bucket.conf (your keys)
WORKER
maand/worker ← build: per-label worker lists, CA cert
maand/worker/<ip> ← build: one host’s metadata
maand/worker/<ip>/tags/<key> ← build: workers.json tags
JOB
maand/job/<job> ← build: catalog metadata (synced)
maand/prometheus ← build: scrape catalog only (when prometheus server job exists)
vars/bucket/job/<job> ← vars.conf + bucket.jobs*.conf (synced)
vars/job/<job> ← hooks only (merge, not wiped)
secrets/job/<job> ← hooks only (encrypted)
ALLOCATION (job on one worker)
maand/job/<job>/worker/<ip> ← build + deploy: certs, peers
Build-owned vs user-owned
| Namespace | On rebuild | Stale keys removed? |
|---|---|---|
maand/bucket, maand/worker*, maand/job/<job> |
Refreshed from workspace/DB | Yes (syncKeyValues) |
vars/bucket, vars/bucket/job/<job> |
Refreshed from vars.conf and bucket.jobs*.conf |
Yes |
vars/job/<job> |
Hook writes only | No (put-only merge) |
secrets/job/<job> |
Unchanged unless hooks/GC | No |
Global (bucket) variables
vars/bucket — your bucket-wide settings
Source: workspace/bucket.conf (TOML).
port_range = "30000,39999"
environment = "production"
log_level = "info"
After maand build, every key lands in vars/bucket.
maand cat kv get vars/bucket environment
# production
Use in templates:
{{ get "vars/bucket" "log_level" }}
maand/bucket — system global metadata
Source: build (not edited directly).
| Key | Example | Meaning |
|---|---|---|
bucket_id |
a1b2c3… |
Bucket UUID |
jobs |
api,worker |
Comma-separated job names |
active_jobs |
api |
Comma-separated job names with at least one active allocation |
<port_name> |
30042 |
Assigned port for jobs with active allocations only |
maand cat kv get maand/bucket bucket_id
maand cat kv get maand/bucket api_http_port
Port numbers live only in maand/bucket. Use get "maand/bucket" "<port_name>" in templates and hooks (including cross-job reads; no demands required).
Worker variables
maand/worker/<ip> — one host
Source: workspace/workers.json + allocation state at build.
| Key | Meaning |
|---|---|
worker_ip |
Host IP |
worker_id |
Stable worker UUID |
hostname |
From workers.json if set |
position |
Order in workers.json |
labels |
Comma-separated labels |
worker_memory_mb |
Declared memory |
worker_cpu_mhz |
Declared CPU |
jobs |
Active job names on this worker |
<label>_peers |
Other workers with the same label |
<label>_allocation_index |
Index among workers with that label |
Example workers.json:
[
{
"host": "10.0.0.1",
"hostname": "node-a",
"labels": ["worker", "api"],
"memory": "8192 mb",
"cpu": "4000 mhz",
"tags": { "zone": "us-east-1a", "rack": "r1" }
}
]
maand cat kv get maand/worker/10.0.0.1 hostname
maand cat kv get maand/worker/10.0.0.1/tags zone
maand/worker — shared across workers (by label)
Source: build aggregates workers per label.
| Key pattern | Meaning |
|---|---|
<label>_workers |
Comma-separated IPs with that label |
<label>_workers_length |
Count |
<label>_0, <label>_1, … |
IP at index |
<label>_label_id |
Stable UUID for the label |
certs/ca.crt |
Bucket CA PEM (for deploy) |
maand cat kv get maand/worker api_workers
# 10.0.0.1,10.0.0.2
Job variables
Three namespaces serve different purposes.
maand/job/<job> — catalog metadata (build)
Source: manifest.json, allocations, resource limits.
| Key | Meaning |
|---|---|
job_id |
Job UUID |
job_name |
Maand job name; Prometheus scrape job_name when _prometheus/scrape.yaml exists |
version |
Target version from manifest |
selectors |
Job selectors |
workers, workers_length, worker_0, … |
Active worker IPs (ordered); disabled allocations omitted |
rollout_order |
Comma-separated active worker IPs for rollout order (synced from catalog on build). Override for one deploy via put_rollout_order in pre_deploy or cli — hook-api.md |
memory, cpu |
Current reservation |
min_memory_mb, max_memory_mb, min_cpu_mhz, max_cpu_mhz |
Manifest bounds |
maand cat kv --jobs api
maand cat kv get maand/job/api workers
maand cat kv get maand/bucket api_http_port
Template (port from global namespace):
{{ get "maand/bucket" "api_http_port" }}
vars/bucket/job/<job> — job config from workspace
Sources:
workspace/jobs/<job>/vars.conf— checked in with the job; fully synced at build.workspace/bucket.jobs.conf(orbucket.jobs.<env>.confwhenjob_config_selectoris set) — bucket overrides per job; wins on key conflict.
# workspace/jobs/api/vars.conf
cluster_name = "prod-east"
feature_flags = "tls,v2"
# workspace/bucket.jobs.conf
[api]
memory = "512 mb"
cpu = "1500 mhz"
replicas_hint = "3"
memory / cpu from bucket.jobs*.conf also drive maand/job/api reservation fields. Other keys are KV-only.
Rebuild replaces the namespace from the merged file contents; keys removed from vars.conf are deleted from KV.
maand cat kv get vars/bucket/job/api cluster_name
maand cat kv get vars/bucket/job/api replicas_hint
See resources-and-placement.md.
vars/job/<job> — runtime config from hooks
Source: hooks — put_job_variable / maand.kv.put in hooks.
# _hooks/hook_setup.py (e.g. post_build)
import maand
def main():
maand.put_job_variable("schema_version", "12")
Rebuild does not delete hook-written keys.
maand cat kv get vars/job/api schema_version
Template:
{{ get "vars/bucket/job/api" "cluster_name" }}
{{ get "vars/job/api" "schema_version" }}
secrets/job/<job> — encrypted secrets
Write only from hooks (put_job_secret). Read with getSecret in templates or the runtime API.
maand.put_job_secret("db_password", "s3cret")
maand cat kv get --reveal secrets/job/api db_password
Never put secrets in vars.conf or the workspace.
Allocation variables (job × worker)
Namespace: maand/job/<job>/worker/<ip>
| Key | Set by | Meaning |
|---|---|---|
certs/<name>.crt, .key |
build | TLS material — certs.md |
<job>_allocation_index |
build | Index among non-removed job peers |
peer_workers |
build | Comma-separated peer IPs for this job (non-removed, includes disabled) |
Target deploy version is not stored here — use job-level maand/job/<job>/version, catalog fields in maand cat deployments, or template context .NewVersion / CURRENT_VERSION / NEW_VERSION in hooks.
maand cat kv get maand/job/api/worker/10.0.0.1 peer_workers
maand cat kv get maand/job/api/worker/10.0.0.1 api_allocation_index
Template (rendered per allocation):
{{ .NewVersion }}
Use template context .WorkerIP so one .tpl works on every worker:
{
"peers": "{{ get (printf "maand/job/api/worker/%s" .WorkerIP) "peer_workers" }}",
"target_version": "{{ .NewVersion }}"
}
End-to-end example
Layout
workspace/
├── bucket.conf
├── bucket.jobs.conf
├── workers.json
└── jobs/
└── api/
├── manifest.json
├── vars.conf
├── Makefile
└── config.json.tpl
bucket.conf
environment = "staging"
port_range = "30000,39999"
workers.json
[
{ "host": "10.0.0.1", "labels": ["worker", "api"], "memory": "4096 mb", "cpu": "2000 mhz" },
{ "host": "10.0.0.2", "labels": ["worker", "api"], "memory": "4096 mb", "cpu": "2000 mhz" }
]
manifest.json
{
"version": "1.2.0",
"selectors": ["worker", "api"],
"resources": {
"memory": { "min": "256 mb", "max": "1 gb" },
"ports": { "api_http_port": {} }
}
}
bucket.jobs.conf
[api]
memory = "512 mb"
vars.conf
service_name = "api-gateway"
config.json.tpl
{
"env": "{{ get "vars/bucket" "environment" }}",
"service": "{{ get "vars/bucket/job/api" "service_name" }}",
"listen": "{{ get "maand/bucket" "api_http_port" }}",
"peers": "{{ get (printf "maand/job/api/worker/%s" .WorkerIP) "peer_workers" }}",
"version": "{{ .NewVersion }}"
}
Build and inspect
maand build
maand cat kv get vars/bucket environment # staging
maand cat kv get maand/job/api workers # 10.0.0.1,10.0.0.2
maand cat kv get vars/bucket/job/api service_name # api-gateway
maand cat kv get maand/worker/10.0.0.1 worker_memory_mb
maand cat kv get maand/job/api/worker/10.0.0.1 peer_workers # 10.0.0.2
Deploy renders config.json.tpl per worker using that worker’s allocation namespace, then rsyncs to /opt/worker/<bucket_id>/jobs/api/.
Runtime update from a hook (pre_deploy):
import maand
def main():
maand.put_job_variable("deployed_at", maand.env("EVENT"))
Persisted when deploy checkpoints KV for that job.
Quick reference: where to put config
| I want… | Put it in… | Namespace |
|---|---|---|
| Bucket-wide setting | workspace/bucket.conf |
vars/bucket |
| Per-job bucket override | workspace/bucket.jobs*.conf |
vars/bucket/job/<job> |
| App config in git | workspace/jobs/<job>/vars.conf |
vars/bucket/job/<job> |
| Hook-written config | hooks | vars/job/<job> |
| Secret | hook hook | secrets/job/<job> |
| Port numbers | manifest.json + build |
maand/bucket |
| Workers / version | manifest.json + build |
maand/job/<job> |
| Host metadata | workers.json + build |
maand/worker/<ip> |
| Peers / certs on one node | automatic at build/deploy | maand/job/<job>/worker/<ip> |
Inspecting KV
maand cat kv # all namespaces (truncated values)
maand cat kv --jobs api # job-related namespaces
maand cat kv --jobs api --workers 10.0.0.1
maand cat kv --active # latest non-deleted versions only
maand cat kv get maand/job/api version
maand cat kv get --reveal secrets/job/api my_secret
Related
- persistence.md — persistence, purge, access control
- templates.md —
get,getSecret, template context - hook-api.md — runtime API for reads/writes
- configuration.md —
bucket.conf,bucket.jobs.conf,vars.conf