id: aws-deploy title: A one-shot Terraform module an operator can consume without cloning this status: open crates: [] dependsOn: [deploy] exitCriterion: > An operator with an AWS account and nothing else pins a module version, supplies a zone name and a container image tag, applies it, and is handed the NS records to delegate — with no file from this repository on their disk. #
aws-deploy #
infra/ lives inside this repository today: a checkout, a terraform apply,
and knowledge of which files matter. The owner asked for a one-shot Terraform
module, published as its own repository, that stands up the whole thing for
an operator who has not cloned this source tree and should not need to.
This project already has the precedent for splitting a component out:
vibescrobble-index, its query service and the canvas were extracted into
their own repository (see the build! commit that removed
crates/vibescrobble-index and the rest, and left index and
canvas in plan/ naming no crates, "for now", until that
repository is published). This epic is the same move applied to infra/:
extract it, version it, and leave a pointer here rather than the Terraform
itself. Whatever conventions that split settled — how the new repository is
named, how it is linked back from here — this epic should follow rather than
invent a second way of doing the same thing.
The design problems, which matter more than the extraction itself #
The module and the binary must stay in step.
infra/templates/user_data.sh.tftpl writes a systemd unit whose ExecStart
passes specific flags — --zone, --owner, --port, --data,
--secret-file today, confirmed by reading that template. A module version and a server (container image) version are
therefore coupled: a flag renamed, dropped, or given new meaning between two
image versions is invisible to Terraform, and an operator who bumps the
module without also picking a compatible image gets a server that will not
start, or worse, one that starts and silently misreads a flag that used to
mean something else. Today nothing states the compatible range in either
direction.
Secrets and Terraform state. infra/variables.tf's ssm_prefix comment
already states the current discipline: secret values are written to SSM out
of band, by a human or a separate process this configuration does not run, so
terraform.tfstate never carries key material. A one-shot module that
generates the attestation secret for the operator — which is the more
"one-shot" reading of the request — breaks that discipline
by construction: anything a Terraform resource creates is a value Terraform's
state file holds, in plaintext, in whatever backend that state lives in
(local disk by default, S3 with no encryption unless configured, a CI
runner's workspace). State is not a secret store anyone chose for this
purpose, and it is routinely committed, shared, or left in a CI cache by
people who never meant it to hold credentials.
What it must output. Above everything else: the NS records the
operator delegates at their parent zone. infra/outputs.tf's
name_servers output already says why in its own comment — Route53 assigns
these when the hosted zone is created, Terraform cannot make the delegation
record itself because the parent zone is not this stack's to manage, and
nothing under the new zone resolves until a human adds that record by hand.
A one-shot module that applies cleanly and prints nothing actionable has
shipped half a deployment.
What it must not assume. infra/variables.tf today requires a vpc_id
and an instance_subnet_id with no default, on purpose — this project has
never assumed an existing VPC. A one-shot module aimed at an operator with
"an AWS account and nothing else" raises the question of whether it should
now provision a minimal VPC itself, and the answer this epic should state
plainly either way rather than let default: it must not assume one exists,
and it must not assume the operator wants Terraform's defaults for anything
that affects blast radius — region, instance size, whether SSH access
exists at all. Offering a minimal-VPC path as one option is reasonable;
making it the only path is not.
Relation to fleet #
fleet is written for an operator running several servers, each
"a whole personal data server in its own right — its own zone, its own
accounts, its own keys, its own repositories." A module the operator
instantiates once per deployment is the natural shape for that: terraform apply with a different root_zone and a different ssm_prefix, ten times,
rather than a script that assumes it is the only deployment on the account.
This epic does not need fleet to exist first, but the module's variables
(anything that would collide across two instantiations in the same account —
IAM role names, KMS alias names, the backup vault name, all of which
infra/'s current .tf files hardcode as didbot-pds* today) need to be
parameterised with that in mind, or a second deployment in the same account
will fail to apply rather than coexist.
Done #
Nothing closed yet.