Manifest overview
How HCL manifests describe the running system.
The shape
Every resource has two labels — scope and name — and a body of typed fields:
kind "scope" "name" {
field = value
nested_block {
other_field = "string"
}
}The pair (kind, scope, name) is the unique key used by diff, apply, and prune.
Kinds
| Kind | Purpose |
|---|---|
deployment | Stateless replicas. The canonical compute kind. |
app | Sugar for deployment + ingress with the same identity (~90% of web shapes) |
statefulset | Stateful pods with stable ordinals + per-pod volumes (postgres, redis, etc.) |
ingress | Caddy route + TLS for a deployment |
job | One-shot task triggered by voodu run |
cronjob | Scheduled task |
asset | Declarative file bundles — file(), url(), inline literals |
registry | Private image pull credentials (host-wide) |
postgres | Postgres macro: statefulset + replication + backups + default probes (via voodu-postgres) |
redis | Redis macro (+ sentinel for HA) + default probes (via voodu-redis) |
mongo | MongoDB macro (planned plugin) |
Cross-cutting blocks
Most kinds (deployment / app / statefulset / job / cronjob) share these optional blocks:
| Block | What it does |
|---|---|
probes | Kubelet-style liveness / readiness / startup health checks |
init | Ordered one-shot prep containers (db:migrate, asset warmup) |
autoscale | CPU-based horizontal scaling (deployment / app only) |
on_deploy | Post-rollout webhook notifications (Slack, PagerDuty, custom) |
release | Once-per-release migrations / hooks (deployment / app only) |
resources | CPU + memory caps |
logs | Docker log driver caps (default 10m × 3) |
build | Build-mode — Dockerfile or auto-detected runtime |
depends_on | Explicit asset dependencies for invisible references |
Reference
- Interpolation reference —
${VAR},${asset.…},{{field}},file(),url() config& secrets — out-of-band env vars, scope/app buckets,env_fromsemantics
Multiple files, one apply
Split your manifests by concern. voodu apply accepts -f repeated:
voodu apply \
-f infra/web.voodu \
-f infra/redis.voodu \
-f infra/pg.voodu \
-r prod-1All files are unioned by (kind, scope, name). Conflicts are a hard error — Voodu won't merge silently.
References across resources
Assets and config bind values back into other resources:
asset "clowk-lp" "acls" {
acls = url("http://internal/users.acl", {
timeout = "10s"
on_failure = "error"
})
}
redis "clowk-lp" "redis" {
volumes = [
"${asset.clowk-lp.acls}:/etc/redis/conf.d/users.conf:ro"
]
}Prune semantics — upsert-only by default
apply only creates and updates. Resources missing from the file are left alone unless you opt in:
- Default:
voodu apply -f voodu.hclupserts everything declared, leaves siblings untouched. - Opt-in:
voodu apply -f voodu.hcl --prunedeletes siblings in the same(scope, kind)that aren't in this apply.
Pruning is per (scope, kind). app is authoring sugar — voodu expands it into a deployment + ingress at parse time, then prunes per canonical kind. So an apply with one app "prod" "api" block + --prune will delete other deployment resources AND other ingress resources in scope prod that aren't declared in the apply. It won't touch cronjob or statefulset siblings.
To keep siblings of the same kind around, declare them in the apply (or split your manifests into separate voodu apply --prune calls).
# Standard apply — upsert-only, safe for multi-repo / multi-file workflows
voodu apply -f voodu.hcl -r prod-1
# Explicit opt-in to clean up siblings
voodu apply -f voodu.hcl --prune -r prod-1Common CI pattern: voodu diff --prune on the PR to surface what would disappear, voodu apply --prune on merge.
Diff semantics
voodu diff -f voodu.hcl -r prod-1Output uses the same + ~ - triplet you see in apply:
~ app/prod/api
~ replicas 1 → 3
~ env.NODE_ENV development → production
+ redis/clowk-lp/redis-ha
+ sentinel.monitor clowk-lp/redis
- cronjob/prod/old-cleanupExit code is 0 (no changes), 2 (changes pending) with --detailed-exitcode, or non-zero on error. Wire it into CI.