Deploy
Operator guide for the League of Agents platform's serverless AWS stack: an API Gateway HTTP API in front of a Lambda WSGI adapter (league_site/aws_lambda/), a single-table DynamoDB match store, an S3 archive bucket, and a Budget alarm pinned to the platform's 20 USD/month ceiling. The infrastructure-as-code lives in infra/ — infra/template.yaml (AWS SAM), the repo-root Makefile (the Lambda build step), and infra/deploy.sh (the one-command wrapper). See architecture for how this stack fits the rest of the platform and operations for the day-2 operator CLI.
What ships here vs. what does not
This doc and the infra/ stack cover the deploy mechanics: build the Lambda package, stand up the AWS resources, and serve the existing league_site.web.http.http_app() WSGI app through them. They do not cover:
- Wiring
league_site.matchespersistence into the deployed Lambda (theMATCHES_TABLE_NAME/ARCHIVE_BUCKET_NAMEenvironment variables and DynamoDB/S3 IAM permissions are provisioned intemplate.yamlahead of that work, butleague_site/aws_lambda/handler.pydoes not read them yet — see that module's docstring). - Cloudflare/DNS routing for
league-of-agents.ai(thecultureflarerunbook referenced in operations). - The
league-site deployCLI verb described in operations — that is a future wrapper around this sameinfra/stack, not a replacement for it.
Honesty note on what is proven here vs. what is not
Deploying from scratch and confirming a no-op redeploy against a real AWS account cannot be proven by this repository's test suite — there is no AWS account, no network access, and no sam CLI available in this task's test environment. What is covered by tests/test_lambda_template.py:
infra/template.yamlparses as valid YAML.- It declares exactly the resources described below (no more, no fewer).
- The
MonthlyBudgetUsdparameter defaults to20and theMonthlyBudgetresource'sBudgetLimitis wired to that parameter. sam validateruns against the template only if thesamCLI is present on the machine running the tests; otherwise that check is skipped, not faked as a pass.
Proving an actual deploy-from-scratch and an actual no-op redeploy against a live AWS account is the live-launch-checklist task's job (see operations's "Launch Checklist"), not this one's.
Prerequisites
- An AWS account and credentials with permission to create the resources listed below (
aws configureor equivalent — this doc does not cover AWS credential setup). - The AWS SAM CLI (
sam --version).infra/deploy.shchecks for it and fails fast with a clear message if it is missing. - GNU Make (
make --version) —infra/template.yaml's Lambda build uses a custom Makefile build method (see "Why a Makefile" below). - Python 3.12 available to the SAM build step (SAM builds the Lambda package using the interpreter on the machine running
sam build, not the one that will run in Lambda — 3.12 keeps them matched). uv sync --extra awsif you also want to run this repo's own test suite locally (tests/test_lambda_template.pyusesboto3's bundled botocore data to sanity-check the template's resource shapes are the ones AWS expects — see that test file for specifics).
Why a Makefile
This project manages dependencies with uv against a PEP 621 pyproject.toml, not a requirements.txt. sam build's built-in Python build workflow only knows how to install from requirements.txt, so it cannot build this function directly. the repo-root Makefile instead runs:
pip install --no-cache-dir --target "$ARTIFACTS_DIR" "$CODE_ROOT"
from the repo root (CODE_ROOT, SAM's resolved CodeUri), which installs league_site and its declared dependencies straight from pyproject.toml via pip's standard PEP 517 build path — no requirements.txt needed, and pyproject.toml itself is untouched.
First deploy
export [email protected] # receives Budget threshold alerts
infra/deploy.sh prod "$BUDGET_ALERT_EMAIL"
This runs sam build (using the repo-root Makefile, see above) followed by sam deploy with:
--stack-name league-of-agents-platform-prod--parameter-overrides StageName=prod BudgetAlertEmail=...--capabilities CAPABILITY_IAM(the Lambda execution role SAM generates)--resolve-s3(SAM manages its own deployment-artifacts bucket)--confirm-changeset(prints the changeset and asks before applying — drop this from a CI pipeline by callingsam deploydirectly with--no-confirm-changesetonce the stack is trusted)
sam deploy writes infra/samconfig.toml on first run, recording the stack name and parameters so later redeploys don't need to repeat them. That file is account/operator-specific and gitignored — do not commit it.
Enabling sessions + GitHub sign-in
infra/template.yaml also declares SessionSecretValue, GithubOauthClientId, and GithubOauthClientSecret parameters, wired into HttpHandlerFunction's environment as LEAGUE_SESSION_SECRET, LEAGUE_OAUTH_GITHUB_CLIENT_ID, and LEAGUE_OAUTH_GITHUB_CLIENT_SECRET. All three default to an empty string, which is what keeps sessions and GitHub sign-in disabled until an operator provisions real values. infra/deploy.sh sources a repo-root .env (gitignored) for these, if present, and maps it onto --parameter-overrides automatically — see runbooks/github-oauth-app.md for registering the GitHub OAuth App, the exact .env variable names, and rotation steps.
On success, the stack outputs the deployed API's base URL (ApiUrl), the DynamoDB table name, and the S3 bucket name. Cloudflare/DNS routing of league-of-agents.ai to that URL is a separate step — see architecture.
Redeploy
infra/deploy.sh prod
Same command, no arguments needed once infra/samconfig.toml exists from the first deploy. sam build re-packages the Lambda function; sam deploy diffs the result against the live stack:
- Something changed (code, template, parameters): a changeset is computed and applied — only the changed resources are touched, matches in DynamoDB and archives in S3 are untouched by a code-only redeploy.
- Nothing changed:
sam deployproduces an empty changeset.infra/deploy.shpasses--no-fail-on-empty-changeset, so this exits0— a redeploy of unchanged code is a safe no-op, not an error.
Teardown
cd infra
sam delete --stack-name league-of-agents-platform-prod
sam delete prompts for confirmation and removes every resource the stack created — the Lambda function, the HTTP API, the DynamoDB table, and (if empty) the S3 bucket. The DynamoDB table and S3 bucket are not retention-protected in template.yaml — deleting the stack deletes match data and archives with it. Export anything worth keeping (e.g. via league_site.matches.aws.S3MatchArchive or a manual aws dynamodb scan / aws s3 sync) before tearing down a stack with real match history.
If the S3 bucket is non-empty, sam delete (via the underlying CloudFormation stack deletion) fails to delete the bucket and leaves it behind — empty it first (aws s3 rm s3://<bucket-name> --recursive) if you want a fully clean teardown.
Where state lives
| Resource | What it holds | Deleted by teardown? |
|---|---|---|
MatchesTable (DynamoDB) | Live and paused match state, single-table design (PK=MATCH#<id>, SK=METADATA) — see league_site/matches/serialization.py | Yes |
ArchiveBucket (S3) | Completed-match JSON archives (archives/{year}/{match_id}.json) and future dataset exports | Yes, if empty (see Teardown) |
HttpHandlerFunctionLogGroup (CloudWatch Logs) | Lambda invocation logs, 14-day retention | Yes |
infra/samconfig.toml | This operator's saved deploy parameters (stack name, stage, budget email) | Not an AWS resource — local file only, gitignored |
infra/.aws-sam/ | sam build's local build cache/output | Not an AWS resource — local directory only, gitignored |
Resources this template declares
Exactly six, each commented in infra/template.yaml against the 20 USD/month ceiling:
HttpApi(AWS::Serverless::HttpApi) — API Gateway HTTP API, cheaper per-request than a REST API and with no per-hour base charge.HttpHandlerFunction(AWS::Serverless::Function) — the Lambda function runningleague_site.aws_lambda.handler.handler,python3.12onarm64(Graviton2, ~20% cheaper per GB-second than x86_64 here).HttpHandlerFunctionLogGroup(AWS::Logs::LogGroup) — 14-day log retention, so CloudWatch Logs storage does not grow unbounded.MatchesTable(AWS::DynamoDB::Table) — on-demand billing (no idle capacity cost) single-table match store.ArchiveBucket(AWS::S3::Bucket) — completed-match archives, with a lifecycle policy moving objects to Standard-IA at 30 days and Glacier at 90.MonthlyBudget(AWS::Budgets::Budget) — an 80%-actual-spend and a 100%-forecasted-spend email alert against theMonthlyBudgetUsdparameter (default20). A Budget is a notification, not an enforcement mechanism — AWS Budgets cannot itself stop a Lambda from invoking or a DynamoDB table from taking writes. The API Gateway throttling limits onHttpApi(burst 20, rate 10 requests/second) are this stack's actual spend circuit breaker; the Budget is the operator's early-warning system on top of it.