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 clihook-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:

  1. workspace/jobs/<job>/vars.conf — checked in with the job; fully synced at build.
  2. workspace/bucket.jobs.conf (or bucket.jobs.<env>.conf when job_config_selector is 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