Use these guides after the quickstart when you need the complete operational sequence or want to extend the template catalog.
This is the multi-page printable view of this section. Click here to print.
Guides
1 - Lifecycle operations
Validate
ainfra validate path/to/document.yaml
ainfra validate path/to/document.json --format json
Validation reads a document but does not resolve secret references or run an infrastructure engine.
Check readiness
ainfra doctor
ainfra doctor --format json
ainfra doctor --input .ainfra/hetzner.input.yaml
ainfra doctor --environment development
The input-aware form also checks backend readiness for the intended environment.
Plan
ainfra plan TEMPLATE --input INPUT
For an initialized project, use the declared environment:
ainfra plan --environment development
Treat the plan as sensitive. Review resource ownership, addresses, firewall rules, image selection, and estimated cost. Apply only the returned plan ID:
ainfra apply TEMPLATE --input INPUT --approve PLAN_ID
The corresponding project-mode apply is:
ainfra apply --environment development --approve PLAN_ID
For the complete project workflow, retain the same review boundary and pass
that exact ID to up:
ainfra plan --environment development
# Review the saved plan, then:
ainfra up --environment development --approve PLAN_ID \
--known-hosts ./known_hosts
up never plans or chooses an approval implicitly. It composes the exact
apply, validated output collection, configuration, and a separate Ansible
check-mode verification stage. If a stage was interrupted after its started
event or ended in failure, up stops and routes the operator to manual
recovery.
The Rust implementation embeds the built-in template and materializes a new
workspace beneath .ainfra/runs/<PLAN_ID>/workspace/; it does not execute
OpenTofu in a source checkout. Initialization uses the committed lock file in
read-only mode. The generated variables, plan bytes, and versioned plan record
remain together in the isolated run directory.
Before apply, ainfra revalidates the input and verifies the operation,
template identity and version, environment, canonical input path and bytes,
retained workspace contents, remote backend configuration path and bytes, plan
location, and plan bytes. Project mode additionally binds the canonical project
root plus the exact ainfra.yaml and ainfra.lock bytes. Any mismatch fails
before OpenTofu initialization or apply begins.
Each completed plan has an immutable run.json and append-only events beneath
.ainfra/runs/<PLAN_ID>/events/. Mutating and configuration phases record a
started event before execution and then a succeeded or sanitized failed
event. A missing terminal event means the process may have been interrupted;
do not infer success or retry automatically.
Inspect local state without contacting a provider:
ainfra status --environment development
ainfra status --environment development --format json
Status validates event ordering and bindings, ignores untrusted run entries, and emits deterministic next steps. A partial, stale, corrupt, or legacy state routes to manual recovery instead of an apply or destroy command.
Teardown uses the same two-step review:
ainfra plan --environment development --destroy
# Review the dedicated destroy plan, then:
ainfra down --environment development --approve-destroy DESTROY_PLAN_ID
An apply-plan ID can never authorize down, and down never runs a direct
unreviewed destroy. A successful destroy process first records
destroy-applied; only a subsequent empty tofu state list for the reviewed
backend records the environment as destroyed.
Read standardized outputs
ainfra outputs TEMPLATE --run PLAN_ID --format yaml
ainfra outputs TEMPLATE --run PLAN_ID --format json
ainfra outputs --environment development --run PLAN_ID
The standardized output is designed for downstream automation. It must not be
confused with raw provider outputs or state. Output collection requires a
successful apply marker for the same exact run and writes
.ainfra/runs/PLAN_ID/output.json.
Configure hosts
ainfra configure TEMPLATE \
--run PLAN_ID \
--known-hosts .ainfra/known_hosts
ainfra configure --environment development \
--run PLAN_ID \
--known-hosts .ainfra/known_hosts
The command generates a run-local inventory using private management
addresses, then runs the retained Ansible playbook. Verify SSH fingerprints
through an independent channel and populate the protected known_hosts file
before running it. Use --check for an explicit Ansible check-mode pass.
Destroy
Create the destroy plan first:
ainfra plan TEMPLATE --input INPUT --destroy
ainfra plan --environment development --destroy
Then approve the returned ownership scope:
ainfra destroy TEMPLATE \
--input INPUT \
--approve-destroy PLAN_ID
ainfra destroy --environment development \
--approve-destroy PLAN_ID
Destroy applies the exact reviewed destroy plan through tofu apply; it never
uses an unreviewed direct tofu destroy operation. An apply plan cannot
authorize destroy, and a destroy plan cannot authorize apply.
After a disposable test, confirm zero owned resources in both Hetzner and OpenTofu state. A successful command without this independent check is not complete teardown evidence.
Direct-tool escape hatch
ainfra remains a thin wrapper. If diagnosis requires direct OpenTofu or
Ansible commands, use the template working directories and preserve the same
input, state, and ownership scope. Never bypass the reviewed-plan or teardown
controls merely for convenience.
2 - Local validation
The repository intentionally runs its gates locally. It does not contain GitHub Actions workflow files.
Install security tools
The supported bootstrap installs pinned tools into the ignored
.ainfra/tools/ directory:
scripts/bootstrap-security-tools
It installs:
- Checkov
3.2.529in an isolated uv tool environment; - Gitleaks
8.30.1, verified against its release checksum.
Checkov is isolated because its dependency constraints conflict with the project environment. Runtime version checks fail closed.
Run all gates
scripts/validate-all
scripts/test-all
The validation suite covers formatting, typing, contracts, OpenTofu,
Ansible, repository policy, Rust Clippy and RustSec checks, Checkov, and
Gitleaks. Rust 1.96.1 is pinned in rust-toolchain.toml; the aibox workspace
installs Rust, Clippy, Rustfmt, and cargo-audit. Missing required tooling is a
failure, not a skipped check.
Build the documentation
docs/scripts/build-docs.sh
docs/scripts/serve-docs.sh
The local server uses
http://localhost:1313/ainfra/ so relative links behave like the
GitHub Pages deployment.
3 - Authoring templates
Read the template strategy first. This guide covers the concrete authoring workflow.
Choose adaptation or a new template
Adapt an existing template when the provider, resource topology, lifecycle, and security model remain the same. Typical adaptations include:
- changing sizes, counts, locations, or opt-in networking through inputs;
- adding another explicitly supported operating-system image;
- extending an existing Ansible role;
- adding a compatible output or capability already understood by ainfra.
Create a new template when the provider changes, the topology has different ownership or teardown behavior, the engine layout changes materially, or the existing template’s security promises would become misleading.
Do not fork a template merely to change an environment value. Copy its example input and keep the template source unchanged instead.
Find and inspect templates
Bundled templates are direct children of templates/. The current catalog is
listed in Template reference.
Inspect the manifest and template README before changing anything:
find templates -mindepth 2 -maxdepth 2 -name ainfra-template.yaml -print
sed -n '1,240p' \
templates/hetzner-kubernetes-baseline/ainfra-template.yaml
sed -n '1,240p' templates/hetzner-kubernetes-baseline/README.md
Adapt the existing template
For an environment-specific adaptation:
- Copy
templates/hetzner-kubernetes-baseline/inputs/example.input.yamloutside the template’s tracked examples. - Keep
metadata.template: hetzner-kubernetes-baseline. - Change only documented input fields.
- Keep credentials as references or environment variables, never values.
- Validate the template and input together.
- Review the generated plan and retain immediate teardown readiness.
cp templates/hetzner-kubernetes-baseline/inputs/example.input.yaml \
/tmp/my-environment.input.yaml
ainfra validate hetzner-kubernetes-baseline \
--input /tmp/my-environment.input.yaml
If the implementation itself must change, edit the OpenTofu, cloud-init, or
Ansible source in the existing directory, increment metadata.version, update
the supported-value documentation, and add tests for the changed behavior.
Create a new template
Start from the nearest existing implementation only when its security defaults are appropriate:
cp -R templates/hetzner-kubernetes-baseline templates/my-template
Then complete every step:
- Rename the directory using lowercase letters, numbers, and hyphens.
- Set the same identifier in
ainfra-template.yamlatmetadata.name. - Reset
metadata.versionfor the new template and describe its purpose. - Declare the supported ainfra wrapper range.
- Point both engine working directories to paths inside the template.
- Reference only shared schemas that are direct children of
schemas/. - Replace example inputs, embedded template identifiers, output metadata, provider resources, and README content.
- Declare only capabilities supported by
src/template.rs. - Pin OpenTofu providers and Ansible collections and roles.
- Add positive and negative contract, policy, engine, and lifecycle tests.
Discovery does not traverse arbitrary paths or accept absolute template
locations. A directory outside templates/, a nested catalog, or a manifest
whose name differs from its directory is rejected.
Required directory contract
The exact implementation directories can vary when declared in the manifest, but a complete template has this shape:
templates/my-template/
├── ainfra-template.yaml
├── README.md
├── inputs/
│ └── example.input.yaml
├── tofu/
│ ├── .terraform.lock.hcl
│ ├── versions.tf
│ ├── providers.tf
│ ├── variables.tf
│ └── outputs.tf
└── ansible/
├── requirements.yml
└── site.yml
Cloud-init, roles, modules, and other source belong under the same template
root. Runtime state and generated output belong under ignored .ainfra/ or
OpenTofu state locations, never in Git.
Validate the result
Run narrow checks while authoring, then the complete repository gates:
ainfra validate my-template \
--input templates/my-template/inputs/example.input.yaml
tofu -chdir=templates/my-template/tofu fmt -check -recursive
tofu -chdir=templates/my-template/tofu init -backend=false
tofu -chdir=templates/my-template/tofu validate
scripts/validate-all
scripts/test-all
Also document the direct OpenTofu and Ansible commands so operators can inspect and troubleshoot without the wrapper. Before claiming provider support, run a cost-approved disposable lifecycle: plan, apply, host verification, Ansible check, apply, idempotence check, destroy, and provider/state confirmation.
Author checklist
- The manifest validates against
template-manifest.v1alpha1.json. - All manifest paths remain inside the allowed template or schema roots.
- Example inputs are useful and contain no secrets.
- Supported values and exclusions are explicit.
- Standard outputs validate as
InfrastructureOutput/v1alpha1and contain no secret-shaped data. - New behavior has positive and negative tests.
- Dependencies and images are pinned.
- Direct engine commands and teardown are documented.
- The full local validation suite is green.
Image choices
Every user-selectable image must list and explain all supported values. The initial template supports:
# Supported images:
# - debian-13: official Hetzner Debian 13 image; the default and only
# initially validated operating system.
image: debian-13
Adding an image is not merely an enum change. It requires hardening, architecture, networking, update, idempotence, and disposable-lifecycle verification.
Keep the contract narrow
Template metadata is declarative and cannot add arbitrary shell hooks. If a new capability is needed, define and test its contract semantics before implementing provider behavior.