These pages explain why ainfra separates project configuration, contracts,
infrastructure state, host configuration, and workload deployment.
This is the multi-page printable view of this section. Click here to print.
Concepts
- 1: Architecture
- 2: Security model
- 3: Project configuration and locking
- 4: State and secrets
- 5: Template strategy
1 - Architecture
Portfolio boundary
ainfra provisions and configures infrastructure targets. It hands a
non-secret output contract to downstream systems:
InfrastructureTemplate + TemplateInput
│
▼
validation and policy
│
▼
OpenTofu plan and state
│
▼
Ansible host configuration
│
▼
InfrastructureOutput
│
▼
workload deployment
OpenTofu owns infrastructure desired state. Ansible owns host configuration.
The ainfra wrapper validates contracts and visibly orchestrates those tools.
Every wrapper operation has a documented direct-tool equivalent.
The first reference implementation is Hetzner-specific and produces Kubernetes-ready hosts. It does not install or claim to operate Kubernetes. Provider abstraction and workload deployment are outside the initial scope.
Public contracts
Three strict JSON Schema draft 2020-12 contracts form the hand-off boundary:
InfrastructureTemplatedeclares engine locations, contract files, capabilities, and mandatory security invariants.TemplateInputcarries non-secret configuration and references to credentials or ignored local configuration.InfrastructureOutputcarries stable target identity, non-secret network facts, inventory metadata, and references to credentials.
Unsupported versions, kinds, capabilities, and unknown fields fail before mutation. Template metadata is not an executable DSL and cannot declare arbitrary shell hooks.
Lifecycle
Planning creates an artifact that can be reviewed independently. Apply accepts the exact plan identifier, not a general confirmation. Destroy accepts the exact reviewed destroy-plan ID, not a generic yes/no flag. These bindings prevent a later command from silently acting on a different resource set.
Local state is permitted only for inputs explicitly marked disposable. Long-lived environments require a capability-validated remote backend with encryption, locking, version recovery, TLS, and access control.
2 - Security model
Assumptions
Cloud credentials, state, plans, private keys, generated inventories, and provider logs may contain sensitive information. The local operator environment is trusted to hold short-lived credentials; the repository and ordinary output documents are not secret stores.
The model reduces accidental exposure and unsafe defaults. It does not turn an untrusted workstation or compromised provider account into a trusted one.
Invariants
- Operators supply public SSH keys;
ainfranever creates private keys. - Root login and password SSH are prohibited.
- SSH host-key checking is mandatory.
- Management ingress is private by default.
- Public IPv4 allocation is opt-in.
- Private networks use narrow RFC1918 ranges and reject broad management ranges.
- Non-disposable environments require encrypted, locked, recoverable remote state with TLS and access control.
- Providers, collections, roles, images, and scanners are pinned.
- Standard outputs contain references to credentials, never their contents.
- Destructive operations name their exact reviewed destroy-plan ID and require explicit approval.
Layered verification
| Layer | What it proves |
|---|---|
| JSON Schema | Document shape, version, enums, and unknown-field rejection |
| Policy checks | Cross-field security invariants |
| OpenTofu validation | Provider configuration and expression correctness |
| Checkov | Known infrastructure-policy findings |
| Gitleaks | Repository secret patterns |
| Plan assertions | The proposed resource graph matches safety expectations |
| Ansible checks | Syntax, lint, check mode, and idempotence |
| Disposable live test | Provider behavior and end-to-end teardown |
No single layer is treated as complete proof. Live Hetzner verification is the final cost-bearing gate and requires explicit approval.
SSH trust
ssh-keyscan can collect a key but cannot authenticate it. Before the first
connection, compare the server fingerprint through a trusted Hetzner console or
another out-of-band channel and place the verified key in known_hosts.
Reporting vulnerabilities
Do not open public issues containing credentials, state, plans, inventories, or exploitation details. Use the repository security contact or GitHub private vulnerability reporting when available.
3 - Project configuration and locking
An ainfra project separates committed intent from local operational state:
my-infrastructure/
├── ainfra.yaml
├── ainfra.lock
├── environments/
│ └── development.yaml
└── .ainfra/
ainfra.yaml names the project, selects one built-in template, and maps
environment names to TemplateInput files. Those files hold non-secret
provider, topology, network, access, and state intent. Credentials remain
external references such as HCLOUD_TOKEN.
ainfra.lock is generated by ainfra init and should normally be committed.
It pins the template source, name, manifest version, and SHA-256 digest of the
complete embedded template. Normal project validation never rewrites it.
.ainfra/ is ignored operational state. Reviewed plans, retained workspaces,
outputs, and other run artifacts belong there, not in committed
configuration.
Discovery and containment
Running ainfra validate without a target searches from the current directory
upward and selects the nearest ainfra.yaml. Project configuration, lockfiles,
and environment inputs must be regular files rather than symbolic links.
Environment paths must be relative and remain inside the canonical project
root.
Lifecycle commands select project mode explicitly with --environment ENV.
The reviewed plan uses ainfra.plan/v1alpha2 and binds the canonical project
root, ainfra.yaml, ainfra.lock, selected input, embedded template, backend,
and OpenTofu plan bytes. Any committed-input change requires a new plan.
The explicit TEMPLATE --input INPUT compatibility mode remains available for
existing development runs. It uses the legacy v1alpha1 record protocol.
Neither record version can authorize execution through the other mode.
Legacy explicit records may remain under the original repository’s ignored
.ainfra/ directory as audit and recovery evidence. They cannot be upgraded
in place, filled with project fields, or used by up or down. Inspect them
with ainfra legacy inspect --root PATH; normal project lifecycle requires a
new v1alpha2 plan bound to the exact project, lockfile, input, template, and
backend configuration.
4 - State and secrets
State
Non-disposable environments must use a capability-validated remote backend providing encryption at rest, locking, version recovery, TLS, and access control. S3-compatible services qualify only after integration tests prove those capabilities.
Local state is limited to inputs explicitly marked disposable and produces a prominent warning. The wrapper does not provision, repair, or silently migrate a backend.
Backend configuration stays outside version control. The repository ignores
local state, plans, .terraform/, and .ainfra/.
Secret references
Contracts accept references:
projectTokenRef:
type: environment
name: HCLOUD_TOKEN
They do not accept a token value. Credentials are resolved only when an underlying tool needs them and are passed through the child-process environment, never command arguments.
Raw state, plans, logs, inventories, and live-verification evidence remain in ignored local storage. Only an explicitly sanitized report may be promoted into documentation.
Recovery
Recovery procedures must identify:
- the backend version to restore;
- the target state lineage;
- the operator authorizing the action;
- the reviewed direct OpenTofu command;
- the verification that follows recovery.
Automatic state repair and migration are intentionally out of scope.
Python-prototype recovery
Treat an old Python checkout and its .ainfra/ directory as read-only
evidence. Start with:
ainfra legacy inspect --root /path/to/old/ainfra-checkout --format json
The report can validate legacy record structure and saved plan bytes, but it
cannot prove whether apply or destroy ran, which backend was active at that
time, or which resources currently exist. Its lifecycle state is therefore
always unknown.
Before any recovery action:
- preserve the original checkout,
.ainfra/, state, and backend metadata; - identify the exact backend, backend version, and state lineage through operator-reviewed backend-native tooling;
- identify the operator authorizing recovery and record a reviewed direct OpenTofu command;
- compare
tofu state listwith provider-owned resources and ownership labels; - initialize a new ainfra project only after its configuration references the same verified backend and intended environment;
- create and review a fresh project-bound plan—never reuse a legacy plan ID;
- independently verify provider resources and state after apply or teardown.
For legacy local state, preserve the old checkout strictly as read-only evidence and use an operator-approved OpenTofu state recovery procedure until the environment is retired or its state has been deliberately moved. The old Python CLI is not a supported recovery runtime. ainfra does not copy, rewrite, import, or infer that state automatically.
5 - Template strategy
An ainfra template is a versioned, self-contained infrastructure implementation behind a common contract. Templates share lifecycle and safety rules, but keep provider-specific OpenTofu, host configuration, and operational documentation together.
Why templates exist
The template boundary separates three concerns:
- The ainfra wrapper owns discovery, contract validation, reviewed-plan binding, sanitized outputs, and ownership-scoped destruction.
- A template owns provider resources, host configuration, supported images, capabilities, and provider-specific defaults.
- The operator owns the selected template, non-secret intent, credential references, reviewed plan, and explicit lifecycle approvals.
This lets a template evolve without hiding OpenTofu or Ansible. Every engine working directory remains directly usable and inspectable.
Source and discovery
All bundled templates are direct children of the repository’s templates/
directory:
templates/
└── hetzner-kubernetes-baseline/
├── ainfra-template.yaml
├── README.md
├── inputs/
├── tofu/
├── cloud-init/
└── ansible/
The directory name is the template identifier. ainfra accepts that
identifier, resolves exactly templates/<identifier>/, and requires an
ainfra-template.yaml whose metadata.name matches the directory. It does
not search arbitrary paths or download templates at runtime.
See the template catalog for the templates currently included in the repository.
What “build” means
Templates are source, not compiled packages. Building a template means assembling and validating a coherent directory:
manifest + input contract + OpenTofu + Ansible + examples + tests
→ contract and path validation
→ static policy and engine validation
→ reviewed OpenTofu plan bound to template content
→ apply and host configuration
→ sanitized InfrastructureOutput
The manifest declares the template version, compatible wrapper versions, engine working directories, input and output schemas, output location, capabilities, and mandatory security properties. During planning, ainfra hashes the complete template directory. Apply refuses to continue if the template, input, environment, version, or reviewed plan bytes have changed.
Shared schemas live in schemas/. Template-specific implementation and
examples stay under the template directory. This prevents a template from
silently weakening the common consumer contract.
Evolution rules
- Patch versions correct behavior without changing the declared contract.
- Minor versions may add compatible options or capabilities.
- Breaking contract changes require the repository’s migration process; the
v1alpha1API cannot be silently replaced. - Provider, module, collection, role, image, and scanner dependencies remain pinned.
- A new capability must be supported by the wrapper before a manifest can declare it.
- Live-support claims require a disposable apply, idempotence check, complete teardown, and redacted evidence.