Quixos OpenTofu
This directory is split into two independent projects:
ami-builder/: submodule that publishes a project-agnostic NixOS EC2 AMI with a matched root snapshot and preseeded ZFS/nixsnapshot.shared-state/: long-lived encrypted EBS volume for state that should survive runtime replacement, such as Caddy ACME account/certificate material and SSH host keys. Runtime deploys attach this volume but do not own or destroy it.runtime/: Quixos deployment stack. It consumes a published AMI for the system image and creates app/runtime resources separately. The deploy wrapper can resolve that AMI from the AMI-builder submodule revision and target architecture.
Destroying the runtime stack destroys the EC2/app/runtime resources only. It
must not deregister the AMI or delete the AMI root or /nix snapshots. Those
belong to the image-builder state. It also must not delete the shared state
volume; that belongs to shared-state.
Shared State
Create the long-lived shared state volume before runtime deploys:
cd tofu/shared-state
tofu init
tofu apply
Use the same aws_region, vpc_id, and subnet_id values as the runtime
stack if you override them. The shared state volume must be in the same
availability zone as the runtime EC2 instance.
Runtime Deploy
Create tofu/runtime/terraform.tfvars:
aws_region = "us-west-2"
route53_zone_id = "Z..."
subdomain = "quixos.example.com"
Then:
cd tofu/runtime
tofu init
./deploy.sh --packages-ref master
deploy.sh resolves the AMI by looking for an owned EC2 image tagged with the
AMI-builder submodule HEAD and target architecture. It writes the resolved
value to resolved-ami.auto.tfvars.json, which is intentionally local state and
is ignored by Git. You can still override it explicitly with --ami-id ami-....
The ami_id Terraform variable has a null default so tofu destroy does not
prompt for an AMI just to destroy resources already recorded in state.
deploy.sh resolves the package-tree ref. If .quixos-package-tree.json
contains quixos.rev, that rev is deployed. Use --quixos-ref master to
override it.
If the package tree or its submodules are private, provide HTTPS credentials
through the wrapper. The token is written to /persist-local/secrets/quixos-git.env
and consumed by the package-tree systemd unit through GIT_ASKPASS.
QUIXOS_PACKAGE_TREE_GIT_USERNAME=oauth2 \
QUIXOS_PACKAGE_TREE_GIT_TOKEN=... \
./deploy.sh --packages-ref master
Runtime Shape
The runtime AMI is expected to provide /nix from the AMI-attached
nixos-zfs-nix ZFS volume from first boot. Runtime user data creates/imports
no filesystems; it is a NixOS flake consumed by the AMI's user-data-flake
service.
The runtime /persist-local volume contains deployment-local Quixos workspace,
secret, and package state. It uses a ZFS pool named
quixos-persist with a quixos-persist/persist dataset. Runtime NixOS
initializes that pool on the attached local persist EBS volume if the volume is
blank, imports it if it already exists, and fails on unknown existing filesystem
signatures.
The /persist mount is reserved for long-lived shared state. It uses the
external shared-state EBS volume and a ZFS pool named quixos-shared with a
quixos-shared/persist dataset. Caddy's data directory is
/persist/var/lib/caddy, and SSH host keys live under /persist/etc/ssh, so
both survive runtime tofu destroy.
The visualizer static derivation uses project-visualizer/yarn-project.nix,
generated by yarn-plugin-nixify.