Skip to the content.

← Docs home

Deploy on AWS (ECS Fargate, Terraform)

The kit runs on AWS as a managed stack: the app, the Upstash-REST shim and the secure-development services on Fargate, behind an ALB with an ACM certificate, over ElastiCache for Redis. terraform apply up, terraform destroy down — still the single-shot lifecycle for an ephemeral event, with no instance to patch.

The module lives at deploy/aws-terraform/; this page is the walkthrough. It stands up the runtime control plane only — provisioning the GitHub org is a separate one-time step (below).

This replaced a single EC2 instance running docker-compose. If you deployed the earlier module, the upgrade is a move rather than an apply: see the migration steps.

Why ECS now, when one EC2 box was the point

The old module’s argument was real and is worth stating before dismantling it: compose on one host needed no translation to task definitions, scoring needed no inbound at all, and a replaced box repopulated its leaderboard from the GitHub PR comments.

What changed the answer is where the event’s data lives. On the box, Redis was a container writing an append-only file to an EBS volume: durability was yours to get right, and a lost volume or a bad fsync was a lost event. That is the wrong thing to hand-roll for a day people have blocked out. ElastiCache makes it AWS’s problem — snapshots, replication, automatic failover across two AZs — and once Redis is managed the rest follows almost for free: the ALB gives native health checks and a task swap without dropping the event, and Fargate removes the instance.

Two things did not change:

What it costs is the honest tradeoff, and the module README itemises it: roughly four times the EC2 box at the defaults. Two variables bring it down if that is too much.

Prerequisites (once, off the stack)

  1. Provision the org from your laptop: ./setup/ctf-setup.sh org (uses your gh auth + local docker login ghcr.io). See the Quickstart.
  2. Pick the domain first, then create the GitHub apps with the OAuth callback at https://<domain>/api/auth/callback/github (ctf-setup.sh app-manifest/app-config and oauth-app/oauth-config).
  3. Store the secrets in SSM Parameter Store as SecureStrings under a path prefix (default /owasp-ctf), each encrypted with the event’s own KMS key (--key-id alias/<name>-secrets). Task definitions reference them by valueFrom, so no secret is ever a plaintext environment variable — the generated Redis AUTH token included, which is why the assembled rediss:// connection string is itself a SecureString rather than an environment entry on the srh task. Because the stack creates that key, this step lands between the two applies below rather than before them. The aws ssm put-parameter list, and why --key-id is not optional, are in the module README.

Deploy

Two things must exist before the rest of the stack can be described: ECR, since the image cannot be named until the registry does, and the KMS key that step 3’s secrets are encrypted with. One targeted apply creates both.

cd deploy/aws-terraform
cp terraform.tfvars.example terraform.tfvars    # edit: domain, github_org, admin_logins
terraform init
terraform apply \
  -target=aws_ecr_repository.main \
  -target=aws_kms_alias.secrets                 # the registry and the secrets key
#   ... now store the secrets (step 3), with --key-id ...
./deploy.sh                                     # build, push
terraform apply                                 # the rest of the stack

Afterwards a redeploy is one command:

./deploy.sh --apply

Terraform creates the VPC (two AZs; a public tier for the ALB and tasks, a private tier for ElastiCache alone), the five security groups above, the ElastiCache replication group with in-transit encryption and an AUTH token it generates for you, the ALB with its ACM certificate, the ECR repository, and the Fargate services for whichever modules this event runs — a quiz-only event brings up no scorer and no poller, the same rule as the compose profiles.

deploy.sh builds and pushes the image; Terraform cannot. The app takes no build-time configuration at all (config v2, #386): github_org and admin_logins are two Terraform variables, set in terraform.tfvars — this path’s equivalent of the wizard’s .env — and mirrored into the app’s task-definition environment the same way scorer_image is. Change either and terraform apply rolls it out; nothing has to be rebuilt or repushed. admin_logins must name at least one login, and github_org is required whenever enable_secure_development is true: sync exits at startup without one, and the app would build fork links with no org to point them at. Both are refused at plan time, not at apply.

The image tag is content-addressed to the git revision, and ECR is set to immutable tags, so re-running with nothing changed reports “already there” and skips the build instead of failing. A dirty apps/web tree gets <revision>-dirty-<digest>, where the digest is taken over the uncommitted build context: change the tracked diff, or the set or contents of the untracked, non-ignored build-input files under apps/webnode_modules and .next are excluded, along with anything else your git ignore rules exclude — and the tag changes with it. That is what keeps a work-in-progress deploy off the tag an earlier one already pushed, which on an immutable registry would have redeployed the earlier image. ./deploy.sh --dry-run prints every command and runs none of them.

Watch a rollout:

aws ecs describe-services --cluster <cluster_name> --services app
aws logs tail /ecs/<name>/app --follow

Both names come from the terraform output.

Tear down

terraform destroy

Notes