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:

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:

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

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:

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:

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

ResourceWhat it holdsDeleted by teardown?
MatchesTable (DynamoDB)Live and paused match state, single-table design (PK=MATCH#<id>, SK=METADATA) — see league_site/matches/serialization.pyYes
ArchiveBucket (S3)Completed-match JSON archives (archives/{year}/{match_id}.json) and future dataset exportsYes, if empty (see Teardown)
HttpHandlerFunctionLogGroup (CloudWatch Logs)Lambda invocation logs, 14-day retentionYes
infra/samconfig.tomlThis 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/outputNot 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:

  1. HttpApi (AWS::Serverless::HttpApi) — API Gateway HTTP API, cheaper per-request than a REST API and with no per-hour base charge.
  2. HttpHandlerFunction (AWS::Serverless::Function) — the Lambda function running league_site.aws_lambda.handler.handler, python3.12 on arm64 (Graviton2, ~20% cheaper per GB-second than x86_64 here).
  3. HttpHandlerFunctionLogGroup (AWS::Logs::LogGroup) — 14-day log retention, so CloudWatch Logs storage does not grow unbounded.
  4. MatchesTable (AWS::DynamoDB::Table) — on-demand billing (no idle capacity cost) single-table match store.
  5. ArchiveBucket (AWS::S3::Bucket) — completed-match archives, with a lifecycle policy moving objects to Standard-IA at 30 days and Glacier at 90.
  6. MonthlyBudget (AWS::Budgets::Budget) — an 80%-actual-spend and a 100%-forecasted-spend email alert against the MonthlyBudgetUsd parameter (default 20). 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 on HttpApi (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.