Skip to content
Français
Cloud medium

Declare a reproducible test environment with feint.yaml

2 min de lecture

Read this page in French

A feint.yaml sitting next to your .tf files replaces the README page where nobody finds the right command again. feint up reads it and brings the whole environment up, feint down takes it back down. The colleague who clones the repository gets the same emulator, the same client variables and the same Terraform, without reconstituting a command line.

What you will get

By the end of this page, your repository will carry its own environment declaration: two commands will bring up an identical lab for everyone, and the emulator will refuse to start rather than deliver an environment different from the declared one.

  • Write the minimal declaration that works.
  • Read the six steps of feint up and what each one establishes.
  • Express what must be true before the environment is called ready.
  • Recognise what this file refuses to carry, and why that is a choice.

The problem: flags that live in shell history

feint serve --vm incus-ovn --contracts contracts is a command somebody types once, half remembers, and types differently the following week. The options that decide what a colleague's emulator can do, namely the runtime, the provider, the contracts and the starting state, end up in a shell history and in a README paragraph. A repository that needs a particular setup then has no way to say so.

The declaration is that way. It makes the environment versioned alongside the infrastructure code it exists to test, and above all read by a command: a declaration nothing reads is a comment.

The shortest file that works

Three blocks are enough. Everything else has a default.

# feint.yaml, next to main.tf
version: 1
cloud:
provider: scaleway
iac:
engine: terraform
directory: .
vars:
endpoint: ${feint.endpoint}

The line to understand is the last one. Many configurations declare an endpoint variable whose default points at 127.0.0.1:4599. Aimed at a port where nothing listens, Terraform blocks up to its own ceiling, the plugin dies, and the error message blames the provider. The ${feint.endpoint} substitution is the only one this file knows: the address is written once, and the engine receives it as TF_VAR_endpoint.

What feint up does, in order

The command runs six steps and says each one out loud. The order is not cosmetic: it puts every possible refusal before the first side effect.

  1. Check what the host can deliver, and refuse before starting. A declaration naming incus-ovn on a machine with no OVN is refused here, naming the missing half, rather than four minutes later half applied.

  2. Start the emulator with the address, the state and the contracts the file names. It is an ordinary feint start, so feint status, feint logs and feint stop all know about it.

  3. Export the client environment from the pack itself, exactly the variables feint env <provider> prints.

  4. Run the engine in the declared directory, in place, with its output passed straight through.

  5. Wait for each ready: condition, each with a deadline and each named while it waits.

  6. Print the endpoints and what proved them.

On the repository's examples/quickstart/scaleway, the end of the output looks like this:

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
address = "203.0.113.2"
- waiting: the emulator answers /instance/v1/zones/fr-par-1/servers
ok: http:/instance/v1/zones/fr-par-1/servers
- waiting: the emulator holds a instance/server
ok: resource:instance/server:1
up: http://127.0.0.1:4599
clients: eval "$(feint env scaleway)"
page: http://127.0.0.1:4599/_feint/ui
proved: http:/instance/v1/zones/fr-par-1/servers, resource:instance/server:1
down: feint down

The proved: line is this command's kept promise. It does not say "it is ready", it says what was checked in order to claim so, condition by condition. The full first run, from configuration to destroy, is in Build your first infrastructure.

The ready: conditions: what must be true to say "ready"

Four forms exist, and all are asserted against the emulator's API, never against Terraform's state file. The difference matters: a state file says what the engine believes it created, the emulator's inventory says what exists.

FormWhat it checks
http:<path>the emulator answers below 400 on that path
tcp:<host>:<port>a connection is accepted
resource:<kind>[:<count>]the inventory holds at least that many objects
service:<resource name>:<port>inside a machine, that port answers

The last form is the newest, and it comes from a dated measurement. One night in the summer of 2026, a stack's four conditions were all confirmed, then 310 milliseconds later the test suite found port 443 closed: cloud-init had not reached the command that starts the service. up had not lied, it had confirmed exactly what it was asked to confirm, and nothing in the file could ask about the inside of a machine. The service: form closes that hole.

A condition in an unknown form is refused at load, with the list of valid forms:

feint: feint.yaml: line 8: `ready`: "ping:127.0.0.1": "ping" is not a ready
condition; the forms are http:<path>, tcp:<host>:<port>,
resource:<kind>[:<count>], service:<resource name>:<port>

The schema refuses by name, never in silence

This is the property that makes the file usable in a team: nothing is accepted and then ignored. The schema is a closed table, and a key it does not know is rejected at load, with the list of keys the block actually takes:

feint: feint.yaml: line 7: unknown field `emulator.paquets`;
`emulator` takes: addr, cleanup, contracts, env, log_level, state

A file that accepts everything and applies half of it is exactly the lie this tool exists to avoid. A typo on contracts costs you one second here, instead of half an hour wondering why responses are not being checked.

What the declaration will never carry

Two boundaries are held on purpose, and knowing them saves you looking for a field that will never exist.

What it isWhat describes itWhere it lives
How to bring the environment upfeint.yamlnext to your .tf files
What the infrastructure isTerraform, OpenTofuyour repository
What the state currently isa snapshotan artefact

The day this file grew a block describing a subnet, it would start rewriting Terraform badly. The day it grew a packages: list, it would start rewriting a development environment manager badly. Four fields are therefore deliberately absent, including expose_to_network: putting an emulator that accepts any credential on the network is a decision the person at the keyboard makes, never a file they cloned.

Asking for less than the file declares

Three forms cover most daily needs, and each announces the difference rather than hiding it.

Fenêtre de terminal
feint up # what the file asks for
feint up --runtime off # deliberately less, and the output says so
feint up --no-iac # the control plane only, without the engine

--no-iac is the useful form when you want the emulator and nothing else, for instance to drive the official CLI by hand. The consequence is stated rather than hidden: the ready: conditions describe what the engine builds, so they are skipped with it, and the summary then prints proved: nothing instead of listing conditions nobody evaluated.

- not waiting: --no-iac skipped terraform, and the ready conditions describe
what it builds (http:/v2/instance, resource:compute-instance:1, …)
up: http://127.0.0.1:4599
proved: nothing

up never downgrades on its own. A declaration naming a runtime the host cannot deliver is refused before anything starts, and the refusal carries the ways out rather than only the wall: run the diagnosis, ask explicitly for less, or change the file. Someone who believes their subnets are separate and finds out otherwise in production is exactly the accident this refusal prevents.

The Exoscale stack runs, from provider 0.71.0

On the repository's Exoscale stack, feint up brings the whole environment up, Terraform included. That was not always true, and knowing it saves you from copying an outdated procedure found elsewhere.

Fenêtre de terminal
cd examples/stacks/exoscale && feint up

From 26 August to 5 September 2026, that same command stopped at the doorstep: the published provider built two clients and only one honoured EXOSCALE_API_ENDPOINT, so an apply or a destroy split between the emulator and a paying account. A feint down run in that directory sent five signed requests to Exoscale's servers.

The fix came from upstream, in version 0.71.0 of the provider, and the refusal turned into a floor: the stack pins >= 0.71.0, and the emulator refuses an older provider by its user agent, naming the version to pin. There is no doorstep veto left for a declaration to hit. The full reasoning is in What feint proves.

The fields you will use most

The exhaustive reference is generated from the schema itself in the repository. These are the ones covering almost every real declaration.

FieldRole
cloud.providerthe pack whose client environment is exported before the engine
cloud.projectsthe projects the emulated account holds, in order
emulator.addrthe listen address, 127.0.0.1:4599 by default
emulator.statethe JSON file the store is loaded from and persisted to
emulator.contractsthe API descriptions every response is checked against
runtime.modeoff, incus, incus-vm or incus-ovn, off by default
runtime.imagesthe machine images this environment needs present
snapshot.loada known state loaded as soon as the emulator answers
iac.engineterraform or opentofu; absent, up brings the control plane up and stops
readywhat must be true before the environment is called ready

The default deserves a sentence: runtime.mode is off because starting machines is a side effect this project asks for rather than assumes. That is what lets the examples run on a CI runner with nothing installed. Moving to real machines is covered in Run real machines.

Troubleshooting

SymptomCauseFix
unknown field at loadA key the schema does not carry, usually a typoRead the list of keys the message prints
Terraform hangs then blames the providerThe endpoint points at a silent portDeclare endpoint: ${feint.endpoint} in iac.vars
up refuses a runtimeThe host cannot deliver the declared modefeint doctor --vm <mode>, or feint up --runtime off
proved: nothing in the summary--no-iac skipped the engine and its conditionsExpected: run without the flag to evaluate the conditions
A FEINT_* variable has no effectIt was exported after the startDeclare it in the emulator.env block
down reports discarded resourcesThe emulator ran without emulator.statefeint snapshot save <name> before stopping

Key takeaways

  • A command reads the declaration: a file nothing reads stays a comment.
  • feint up refuses before acting, which puts every possible failure before the first side effect.
  • ready: conditions ask the emulator, never the engine's state file.
  • The schema is a closed table: an unknown key is rejected by name, with the list of valid ones.
  • The declaration does not describe the infrastructure: that boundary with Terraform is held on purpose.
  • runtime.mode defaults to off, because starting machines is asked for explicitly.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn