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/pds/ 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 ff72f82f, the build! commit that removed
didbot-index, didbot-query and the canvas, 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/pds/:
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/pds/templates/user_data.sh.tftpl writes a systemd unit whose ExecStart
passes specific flags — --zone, --operator, --port, --data, --tls,
--acme-environment, --route53-zone-id 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/pds/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 that
delegate the new zone at its parent. infra/pds/outputs.tf's
name_servers output already says why in its own comment — Route53 assigns
these when the hosted zone is created, and nothing under the new zone
resolves until an NS record naming them exists at the parent. Under did.bot
that record is infra/site/'s pds_zone_name_servers, so the delegation is
an apply; under any other parent it is a visit to that zone's own provider.
Either way the module has to hand the operator the four names: a one-shot
module that applies cleanly and prints nothing actionable has shipped half a
deployment.
What it must not assume. infra/pds/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/pds/'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.