Infrastructure-as-code for running lance.blue
HCL 45%
Python 29%
Shell 21%
JavaScript 5%

README.md

infra #

AWS infrastructure for lance.blue, as Terraform. Separate from the code repos on purpose: it is the only place that knows account ids, region and DNS, so nothing else has to.

The AWS account is 179302349187. It contains only lance.blue. Everything written here is applied.

repo what it is
arena the match container — MegaMek plus Suramadu, one game per image. Publishes to the ECR repository this repo creates.
headquarters control plane: launches matches, holds OAuth sessions against players' own PDSes, serves the AppView
infra this repo
helm a queryable database of MegaMek's unit library, indexed in SQLite
lexicons ATProto lexicon definitions for lance.blue
permadeath_games_lexicons ATProto lexicons for games.permadeath.*, an identity and social layer for games
sds artificial intelligence for playing BattleTech matches via MegaMek

How it is run #

By hand, from a laptop. There is no CI, no OIDC role and no credential in the repo. Applies run as lance-blue-apply, a role created by envs/bootstrap/ and assumed through the lance-blue profile. Anything sensitive is passed as -var on the command line.

export AWS_PROFILE=lance-blue AWS_REGION=us-east-1
tofu -chdir=envs/lance.blue plan -out=lance.blue.tfplan
tofu -chdir=envs/lance.blue apply lance.blue.tfplan

Applying the saved plan means what you read is what runs.

scripts/deploy.sh is those two commands plus one confirmation, and it posts to lance.blue's Bluesky account when a release id moved — a public record of every deploy, built from the outputs the apply produced:

scripts/deploy.sh

The post carries a link card for the site and the commits behind the ids that moved, read out of the sibling checkouts, as a thread when they do not fit in one post.

The app password it posts with comes from the login keyring, not from this repo and not from the AWS account — see the runbook.

See docs/runbook.md for first-time setup, the bootstrap step and what to do when something is wrong.

Pre-commit hooks #

Hooks are managed by prek and configured in prek.toml. One-time setup:

uv tool install prek
prek install --prepare-hooks

They then run on every commit; prek run --all-files runs them by hand. Formatting and validation live here rather than in scripts. Both come from pre-commit-terraform: terraform_fmt rewrites unformatted files, and terraform_validate init-and-validates the roots holding the files that changed, without touching AWS. A local tofu-validate-callers hook adds back the one thing per-root validation misses — a module edit that breaks the env roots composing it.

Layout #

envs/lance.blue/ the AWS account. Composes modules; holds the backend config and all real values.
modules/network/ VPC, public subnets, no NAT gateway
modules/ecr/ image repositories, one per name in a list, with a lifecycle policy
modules/artifacts/ S3 for scenarios, camo and match results
modules/match-cluster/ ECS cluster and the arena task definition
modules/static-site/ S3 + CloudFront for the headquarters frontend
modules/dns/ the lance.blue hosted zone
modules/api-host/ the EC2 host running headquarters-api behind Caddy
envs/bootstrap/ one-shot: the state bucket and the apply role
scripts/ deploy, logs, a shell on the api host, match operations, per-match cost, health checks
docs/ architecture, runbook

Modules take inputs and return outputs; they never read remote state, and the one place a module calls aws itself is the static-site cache invalidation. Each directory under envs/ is one AWS account. Dev and prod resources share the account and the root, told apart by name suffix; some resources (the hosted zone, ECR) are shared between environments. A second AWS account would be a directory copy.

Conventions #

  • Names say what the thing is and end with the environment: lance-blue-match-task-sg-prod, lance-blue-web-bucket-prod-<account>. Shared resources — the hosted zone, the ECR repositories — carry no environment. Only prod exists today.
  • Tags come from default_tags on the provider: Project=lance.blue, ManagedBy=terraform, Repo=infra, and Commit when the sha is passed at plan time (-var git_commit=$(git rev-parse --short=12 HEAD); omitted, the tag is simply absent). The env root passes Component — and, for per-environment resources, Environment — into each module's tags.
  • Regions: one, from var.region. Anything that must live in us-east-1 regardless (CloudFront-facing certificates) uses an aliased provider and says so.
  • No secrets in this repo. Not in variables, not in .tfvars, not in state if it can be avoided. A secret is passed as -var or TF_VAR_* and never written to disk.
  • Variable files are committed, split by who edits them: terraform.tfvars is hand-edited constants, releases.auto.tfvars.json is what is deployed and is rewritten by a deploy.

Licensing #

See discussion in the headquarters repo.