This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Concepts

Architecture, security, and state boundaries.

These pages explain why ainfra separates project configuration, contracts, infrastructure state, host configuration, and workload deployment.

1 - Architecture

Tool ownership, public contracts, and lifecycle boundaries.

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:

  1. InfrastructureTemplate declares engine locations, contract files, capabilities, and mandatory security invariants.
  2. TemplateInput carries non-secret configuration and references to credentials or ignored local configuration.
  3. InfrastructureOutput carries 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

Threat assumptions, enforced invariants, and verification layers.

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; ainfra never 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

LayerWhat it proves
JSON SchemaDocument shape, version, enums, and unknown-field rejection
Policy checksCross-field security invariants
OpenTofu validationProvider configuration and expression correctness
CheckovKnown infrastructure-policy findings
GitleaksRepository secret patterns
Plan assertionsThe proposed resource graph matches safety expectations
Ansible checksSyntax, lint, check mode, and idempotence
Disposable live testProvider 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

Committed desired state and generated template provenance.

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

Backend requirements, secret references, and recovery.

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:

  1. preserve the original checkout, .ainfra/, state, and backend metadata;
  2. identify the exact backend, backend version, and state lineage through operator-reviewed backend-native tooling;
  3. identify the operator authorizing recovery and record a reviewed direct OpenTofu command;
  4. compare tofu state list with provider-owned resources and ownership labels;
  5. initialize a new ainfra project only after its configuration references the same verified backend and intended environment;
  6. create and review a fresh project-bound plan—never reuse a legacy plan ID;
  7. 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

How ainfra turns versioned template source into reviewable infrastructure.

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:

  1. The ainfra wrapper owns discovery, contract validation, reviewed-plan binding, sanitized outputs, and ownership-scoped destruction.
  2. A template owns provider resources, host configuration, supported images, capabilities, and provider-specific defaults.
  3. 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 v1alpha1 API 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.