OpenTofu runner and state
How OrkStage runs OpenTofu in throwaway containers, keeps one S3 state per resource without secrets, plans before apply and scans for drift.
OrkStage provisions AWS with OpenTofu modules in infra/tofu/modules/aws/*, one module per logical
resource type.
ADR 0005 records the
design.
The isolated runner
- The worker never runs OpenTofu in its own process. Every
tofucommand runs in a throwaway container: the pinnedghcr.io/opentofu/opentofu:1.12.7image, or thetofu-runnerimage with the modules baked in. - The module library is mounted read only, or baked into the image. Providers come from a plugin cache volume that all runners share.
- Credentials reach that one container only, as environment variables. They never appear in files, command lines or state.
- Each command has a hard timeout (
TOFU_TIMEOUT_MINUTES, 45 by default) and can be cancelled.
Planned before production
Today the worker starts runners through the Docker socket, which gives it control of the host's Docker daemon. A runner that uses ECS RunTask or a Kubernetes Job with a scoped role is on the roadmap.
One state per resource
The state key is orkstage/<physical-name>.tfstate. There is one state for each resource, not one
for each instance or placement slot. This design:
- keeps the platform's own DAG, with progress, retries and failure isolation for each resource;
- limits the damage of a broken state file to one resource;
- passes dependencies as module outputs that become inputs of the next module. Every module emits a
kindoutput, so the adapter wires them by type; - stores the exact root configuration next to the state (
.config.json), so a destroy never depends on a template that has changed since.
Backend: S3, with use_lockfile for locking, and encryption. On real AWS, you create the bucket
in advance with versioning and encryption, and OrkStage validates it before use (see
Connecting an AWS account). On an
emulator, OrkStage creates the bucket on first use.
No secrets in state
An OpenTofu ephemeral resource generates each RDS master password, and the module writes it
through write only attributes (password_wo, secret_string_wo). Services receive the ARN of the
Secrets Manager secret as an ECS secret. We verified that the state holds password: null.
Provider plan before apply
In the Generate plan phase, the worker runs a real tofu plan for every existing resource
whose desired state changed. It writes a summary for each resource to the deployment's event log:
INFO [phase:plan] api: 0 to add, 1 to change, 0 to destroy
WARN [phase:plan] ingress: 1 to add, 0 to change, 1 to destroy; replace aws_lb_target_group.route[0]- A deletion or replacement makes the deployment high risk. A routine new revision of an ECS task definition (for a new image digest) does not.
- For a
productioninstance, high risk escalates the deployment toAWAITING_APPROVAL, even when the platform's own diff predicted only an update. - A resource the provider could not plan also escalates a
productiondeployment to approval: nobody approves changes nobody has seen. - When no change is destructive, the approval step is marked skipped, with the reason.
- The deployment stores the provider plans (
plan.providerPlans) for review.
What is approved is what is applied
Every OpenTofu command runs tofu validate after init, so a configuration error stops before any
plan or change.
The plan phase saves each provider plan to a file named after the deployment, in that resource's
working directory, next to a hash of the configuration it was made from. The apply phase then runs
tofu apply <plan file> instead of planning again:
- If the configuration changed in between (for example a dependency was replaced earlier in the same deployment), or the state changed (someone deployed or edited the resource), a production step fails with a message that says so, and nothing is applied. Deploy again to plan and approve the current changes. Other environments log a warning, plan again and apply.
- The plan file holds attribute values, so it is used once and deleted, whether the apply worked or not. Retries of a failed step plan again.
- Resources this deployment creates have no saved plan: the platform plan already lists them as new.
Previews and drift scans plan in working directories of their own, without the state lock, so they never touch the directory of a running deployment.
Releasing a stale state lock
Locking uses a lock file next to the state in the bucket (use_lockfile). A worker that is killed in
the middle of an apply can leave it behind; the next deployment of that resource then fails with
Error acquiring the state lock and prints the lock id. Before releasing it, make sure no
deployment of that instance is still running. Then, from the worker host:
docker run --rm -it -v orkstage-tofu-work:/workspace -w /workspace/<directory> -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN ghcr.io/opentofu/opentofu:1.12.7 force-unlock <lock id>The directory is the resource's working directory under TOFU_WORK_DIR (its name is printed in the
failing step's log), and the credentials are the target's (for AWS, a session of the deployment role).
Write down who released which lock and why in the instance's incident or change record: the release
itself happens outside OrkStage, so the audit log does not see it.
Drift scans
Every DRIFT_CHECK_INTERVAL_MINUTES (360 by default, 0 turns scans off), the worker plans each
applied resource on real providers again, from the snapshot it was deployed with. Any planned action
means someone changed the resource outside OrkStage. The resource then becomes DRIFT_DETECTED,
OrkStage writes an audit event and sends instance.drift_detected. The scan skips resources with
pending changes. A new deployment restores the configuration from the template.
Recovery from deleted resources
When someone deletes a resource outside OrkStage, its recorded state outlives it. The AWS adapter has a live existence probe for this case:
- During verification, OrkStage probes any resource that is not healthy. If the resource no
longer exists, the step fails at once with
Resource no longer exists at the provider. Redeploy to re-create it.instead of waiting. - Verification waits in one settle window of 5 minutes per deployment, not one per resource, so a slow service does not multiply the wait.
- On the next deployment, the executor asks the provider whether converged resources still exist. It recreates the missing ones instead of skipping them as unchanged.
Verified against MiniStack
The AWS adapter has been tested end to end against MiniStack with: VPC, subnets and security groups,
ECR, SQS with a dead letter queue, S3, Secrets Manager, RDS Postgres (real container), an ElastiCache
Valkey replication group (real container), an ALB with target groups and listener rules (live data
plane at /_alb/<name>/), an ECS Fargate service (real container, registered in the target group and
healthy), Route 53 and ACM. The adapter contract suite passes against it.
Secret references
OrkStage stores references to secrets, not values. The env and AWS Secrets Manager schemes, JSON keys, emulator settings and where each secret lives.
Security hardening
Security controls built into OrkStage, a hardening checklist for hosting it yourself, the known gaps and how to report a vulnerability.