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

KindPurpose
deploymentStateless replicas. The canonical compute kind.
appSugar for deployment + ingress with the same identity (~90% of web shapes)
statefulsetStateful pods with stable ordinals + per-pod volumes (postgres, redis, etc.)
ingressCaddy route + TLS for a deployment
jobOne-shot task triggered by voodu run
cronjobScheduled task
assetDeclarative file bundles — file(), url(), inline literals
registryPrivate image pull credentials (host-wide)
postgresPostgres macro: statefulset + replication + backups + default probes (via voodu-postgres)
redisRedis macro (+ sentinel for HA) + default probes (via voodu-redis)
mongoMongoDB macro (planned plugin)

Cross-cutting blocks

Most kinds (deployment / app / statefulset / job / cronjob) share these optional blocks:

BlockWhat it does
probesKubelet-style liveness / readiness / startup health checks
initOrdered one-shot prep containers (db:migrate, asset warmup)
autoscaleCPU-based horizontal scaling (deployment / app only)
on_deployPost-rollout webhook notifications (Slack, PagerDuty, custom)
releaseOnce-per-release migrations / hooks (deployment / app only)
resourcesCPU + memory caps
logsDocker log driver caps (default 10m × 3)
buildBuild-mode — Dockerfile or auto-detected runtime
depends_onExplicit asset dependencies for invisible references

Reference

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-1

All 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.hcl upserts everything declared, leaves siblings untouched.
  • Opt-in: voodu apply -f voodu.hcl --prune deletes 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-1

Common 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-1

Output 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-cleanup

Exit code is 0 (no changes), 2 (changes pending) with --detailed-exitcode, or non-zero on error. Wire it into CI.

On this page