Resources Overview
The six built-in resources and how they work.
Resources are the building blocks of Ignition recipes. Each resource manages a specific aspect of server state.
The six resources
| Resource | Purpose | Nature |
|---|---|---|
apt | Manage system packages | Declarative |
docker | Manage Docker containers | Mixed |
file | Manage file contents, permissions, ownership | Declarative |
directory | Manage directories | Declarative |
exec | Run arbitrary commands | Imperative |
service | Manage systemd services | Mixed |
Declarative vs imperative
Declarative resources describe a desired end state. Ignition checks the current state and only makes changes if needed:
// Declarative: "nginx should be installed"
await apt({ name: "nginx", state: "present" })
// If already installed → ok (no action)
// If not installed → installs it → changedImperative resources always execute their action:
// Imperative: "run this command"
await exec({ command: "npm install" })
// Always runs → always reports changedThe service resource is mixed — started and stopped are declarative (they check first), while restarted and reloaded are imperative (they always execute).
The docker resource is also mixed — the lifecycle states are declarative by default, but
pull: "always" intentionally forces apply mode to refresh the image reference on every run.
Creating resources
Use createResources(ctx) to get bound resource functions:
import type { ExecutionContext } from "@grovemotorco/ignition"
import { createResources } from "@grovemotorco/ignition"
export default async function (ctx: ExecutionContext) {
const { apt, docker, file, directory, exec, service } = createResources(ctx)
// Each function is bound to the current ExecutionContext
await apt({ name: "curl", state: "present" })
await docker({ name: "cache", image: "redis:7" })
}Resource results
Every resource call returns a ResourceResult:
const result = await apt({ name: "nginx", state: "present" })
result.status // "ok" | "changed" | "failed"
result.type // "apt"
result.name // "nginx"
result.durationMs // execution time in milliseconds
result.current // observed state before execution (undefined on failure)
result.desired // target state from input (undefined on failure)
result.output // resource-specific output data (undefined on failure)Status meanings
| Status | Meaning |
|---|---|
ok | Already in the desired state. No changes made. |
changed | Was not in desired state. Changes were applied (or would be, in check mode). |
failed | An error occurred during check or apply. |
Resource call metadata
Pass metadata as the second argument to tag, identify, or annotate resource calls:
await apt({ name: "nginx", state: "present" }, { tags: ["web"], id: "install-nginx" })| Field | Type | Purpose |
|---|---|---|
tags | string[] | Call-level labels for programmatic filtering |
id | string | Unique identifier for this call |
notify | string[] | Notification labels (for future use) |
sensitivePaths | string[] | Redaction hints; not automatically enforced by the current CLI |
CLI --tags filtering applies to recipe meta.tags, not per-resource call metadata.
Check-then-apply lifecycle
Every resource follows a two-phase cycle managed by executeResource():
check()— inspects current state and decides whether changes are needed. For built-in resources, this phase is read-only and safe to run underignition run --check.apply()— mutating. Only called ifcheck()returnedinDesiredState: falseand the mode isapply.
This gives you:
- Safe dry-runs —
ignition run --checkrunscheck()for every resource without callingapply() - Safe re-runs — if everything is already correct, nothing changes
- Explicit failures — any resource that can't converge reports
failed
Imperative resources can still be conservative. For example, unguarded exec, guarded exec
with apply-time preconditions (unless / onlyIf), and service with state: "restarted" or
"reloaded" may report "would change" in check mode because they cannot prove the final outcome
from read-only checks alone.
exec also exposes unsafeCheckUnless and unsafeCheckOnlyIf as explicit escape hatches when you
intentionally want a command to run during check() and accept dry-run side effects.
When an exec command needs setup such as sourcing NVM or exporting vars, use shell to run a
POSIX shell preamble before the main command. The preamble shares the same shell session as the
command, and the same sudo, cwd, and env settings also apply to unless / onlyIf guards.
For the full API of each resource, see the individual reference pages: