Skip to content

Deploy the AWS infrastructure

QuOptuna deploys as a single stoppable EC2 instance running the application container behind Caddy. Supabase stores application and Optuna data, and S3 stores datasets and analysis artifacts. There is no Kubernetes cluster, load balancer, NAT Gateway, RDS instance, or open SSH port.

Terraform lives in infra/terraform/ (a foundation stack for persistent resources and an application stack for compute), and every operation is driven by the scripts in infra/scripts/.

RequirementNotes
Terraform>= 1.10.0, < 2.0.0 (pinned in versions.tf)
AWS CLI v2Authenticated; aws sts get-caller-identity must succeed
Docker with BuildxUsed to build and push the immutable application image
jqRequired by the operation scripts
gitUsed to stamp the deployed image
curl, Python 3Health checks and deployment-file parsing

The scripts check for aws, terraform, jq, docker, and git, and fail with Required command not found if any is missing.

  • AWS account with permissions for EC2, S3, ECR, Route 53, Secrets Manager, IAM, and SSM.
  • Supabase PostgreSQL URL. The EC2 network is dual-stack, so Supabase’s IPv6-only direct endpoint works. An IPv4-compatible session-pooler URL with sslmode=require is also supported.
  • A domain registered anywhere and delegated to an existing Route 53 hosted zone. You need the zone ID.
  • An Auth0 application (see Auth0 setup below).
  • A globally unique S3 bucket name for Terraform state. The scripts create the bucket if it does not exist, with versioning, encryption, and public-access blocking enabled.

Copy the template and fill it in:

Terminal window
cp .env.deploy.example .env.deploy
aws sts get-caller-identity

infra/scripts/envfile.py parses this file as a conservative KEY=VALUE subset — it never evaluates shell syntax. Matching variables already present in your process environment take precedence over the file.

Deployment keys (control where and how infrastructure is built):

VariableExamplePurpose
AWS_PROFILEdefaultProfile for the AWS credential chain
AWS_REGIONus-east-2Target region
TF_STATE_BUCKETGlobally unique Terraform state bucket
PROJECT_NAMEquoptunaResource name prefix
DOMAIN_NAMEquoptuna.example.comPublic hostname served over HTTPS
ROUTE53_ZONE_IDZ0000...Hosted zone for the domain
INSTANCE_TYPEt3.largeEC2 instance size
ROOT_VOLUME_SIZE50Root EBS volume in GB

Runtime keys (written into the AWS Secrets Manager runtime secret):

DATABASE_URL, OPTUNA_DATABASE_URL, OPTUNA_DB_SCHEMA, AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_SECRET, AUTH_ALLOWED_EMAILS, AUTH_REQUIRE_VERIFIED_EMAIL, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY.

See the Configuration reference for what each runtime variable does.

In the Auth0 application, set:

  • Allowed Callback URL: https://YOUR_DOMAIN/auth/callback
  • Allowed Logout URL: https://YOUR_DOMAIN
  • Allowed Web Origin: https://YOUR_DOMAIN

Then open Actions → Library → Build Custom, create a Post-Login Action, and paste infra/auth0/approved-emails.js. Add these Action secrets:

  • QUOPTUNA_CLIENT_ID — the QuOptuna Auth0 client ID
  • ALLOWED_EMAILS — the same comma-separated list as AUTH_ALLOWED_EMAILS

Deploy the Action and add it to Actions → Flows → Login. QuOptuna repeats the same allowlist and verified-email checks in the API, so removing the Action does not open application access.

Terminal window
uv sync
uv run quoptuna infra --environment dev --env-file .env.deploy

This opens a Textual console with an action sidebar, a live status panel, and a streaming log. Secrets matching password, secret, token, database_url, access_key, or private_key are redacted from the log output.

OptionDefaultPurpose
--environment, -edevTarget environment (dev or production)
--env-filenoneDeployment .env file; falls back to the process environment
--terraform-dirinfraDirectory holding scripts/ and terraform/

Key bindings: r refreshes status, q quits. Only one operation runs at a time. Pause asks for confirmation; Destroy requires you to type the environment name exactly.

ActionWhat it does
CreateState bootstrap, persistent resources, image build, EC2, DNS, and HTTPS
DeployBuild and deploy a new immutable image
UpdateApply Terraform changes and deploy the new image
PauseRefuse while work is active, remove DNS, and stop EC2
ResumeStart EC2, restore DNS, and wait for HTTPS
StatusReport EC2 state, app health, image, and active work
DestroyDelete compute/network resources, preserving Supabase and AWS data

The same operations run directly as scripts:

Terminal window
infra/scripts/create.sh dev --env-file .env.deploy
infra/scripts/status.sh dev --env-file .env.deploy --json
infra/scripts/pause.sh dev --env-file .env.deploy
infra/scripts/resume.sh dev --env-file .env.deploy
infra/scripts/deploy.sh dev --env-file .env.deploy
infra/scripts/update.sh dev --env-file .env.deploy
infra/scripts/destroy.sh dev --env-file .env.deploy
FlagApplies toPurpose
--env-file PATHallDeployment file to read
--jsonstatusEmit machine-readable status (used by the console)
--plan-onlycreate, updateShow the Terraform plan without applying
--forcepause, destroyProceed even though work is active
--confirm-destroydestroySkip the interactive destroy confirmation
--delete-datadestroyAlso delete the persistent AWS foundation

Use --force only when you intentionally accept interrupting active trials.

To delete the persistent AWS foundation too:

Terminal window
infra/scripts/destroy.sh dev --env-file .env.deploy --delete-data

This requires two typed confirmations. It deletes the artifact bucket, images, and runtime secret. It never deletes Supabase or the Terraform-state bucket.

Two CLI commands report on a running deployment, both emitting JSON:

Terminal window
quoptuna active-work # active optimization and analysis counts
quoptuna deployment-check # readiness checks; exits 1 when unhealthy

pause calls active-work on the instance over SSM to refuse stopping EC2 while trials are still running. See the CLI reference.

  • Pause EC2 whenever trials are not running.
  • The default t3.large uses standard CPU credits, preventing unlimited-credit charges.
  • No Elastic IP is retained while paused; DNS is restored to the new address on resume.
  • ECR keeps only five images.
  • Old S3 artifacts transition to Glacier Instant Retrieval.
  • Container logs rotate locally; CloudWatch log ingestion is not enabled.
  • Increase INSTANCE_TYPE only for trials that need more CPU or memory.

If a deployment fails, inspect status and use SSM without opening SSH:

Terminal window
aws ssm start-session --target INSTANCE_ID
MessageCause
Required command not found: XInstall the missing tool from Tooling
AWS credentials are unavailableaws sts get-caller-identity fails; check AWS_PROFILE
Set X in ...A required deployment or runtime key is missing
Script is missing or not executableRun chmod +x infra/scripts/*.sh
Environment file not foundThe --env-file path is wrong