docs/deploy/aws.md

Deploy on AWS

One module call stands up one Vocion installation in your AWS account. You own the account, the data and the keys.

Get the module

The module is infra/terraform/modules/vocion-stack in vocion-core. Pin vocion-core as a submodule of your infrastructure repo so the infrastructure and the application move on one pin, and confirm the tag you pin contains the module (git -C vocion-core ls-tree <tag> infra/terraform/modules/vocion-stack). It needs OpenTofu 1.6+ and the AWS provider >= 5.70, < 6.0; region and credentials come from your provider block.

What it builds

Default
NetworkVPC; public subnets for the load balancer and host; private DB subnets with no route out
EdgeALB with an ACM certificate (DNS-validated in your zone), HTTPS only, WAF with AWS managed rules and a per-IP rate limit
HostOne EC2 (Amazon Linux 2023, encrypted, IMDSv2), r6i.large by default; reachable only from the ALB; access by SSM Session Manager, no SSH
DatabaseRDS PostgreSQL 16 with pgvector, TLS forced, encrypted with a customer-managed key, point-in-time recovery, deletion protection
Credential vaultA KMS key; the app runs VOCION_CREDENTIAL_VAULT=kms
MediaA private, versioned S3 bucket
SecretsTwo Secrets Manager entries, created empty — no secret value is ever in state
BackupAWS Backup daily into a vault locked in governance mode
Logs and alarmsALB, WAF and VPC flow logs (90 days); SNS alarms for the host, the site, 5xx and RDS
ModelsThe host's role may invoke Anthropic models on Bedrock (bedrock_enabled, default on)
OptionalEngineering runners on Fargate (runners_enabled), AgentCore IAM (agentcore_enabled), a monthly budget alarm

It does not create your hosted zone, put secret values, self-host Langfuse, or deploy an AgentCore runtime.

The module call

module "vocion" {
  source = "./vocion-core/infra/terraform/modules/vocion-stack"

  name_prefix     = "northwind-vocion"
  azs             = ["us-east-1a", "us-east-1b"]
  hostname        = "agents.northwind.example"
  route53_zone_id = aws_route53_zone.agents.zone_id   # null: add the validation records yourself
  core_ref        = "v5.0.0"                          # tag or 40-char commit; a branch is rejected

  app_env = {                       # non-secret settings only (stored in SSM)
    VOCION_SCHEDULE_OWNER           = "1"
    VOCION_ENFORCE_WORKSPACE_ACCESS = "1"
    LANGFUSE_ENABLED                = "false"
  }

  alarm_emails = ["ops@northwind.example"]
}

Set VOCION_SCHEDULE_OWNER = "1" yourself: the module doesn't, and without it no schedule, automation or workflow runs. Useful switches: instance_type, db_instance_class, db_multi_az, alias_hostnames (old hostnames 301 to the new one, /api/* served in place), waf_rate_limit, backup_retention_days (35), runners_enabled, budget_monthly_usd. The module README lists all inputs and outputs.

First deploy

tofu init && tofu apply

The host boots, finds empty secrets and waits.

1. App secrets. At least AUTH_SECRET and a separate VOCION_TOOL_SIGNING_SECRET (openssl rand -base64 32 each), plus your model keys if you don't use Bedrock:

aws secretsmanager put-secret-value \
  --secret-id "$(tofu output -raw app_env_secret_name)" \
  --secret-string file://app-env.json

2. The app's database login. Port-forward to RDS through the host and connect as the master user:

aws ssm start-session --target "$(tofu output -raw instance_id)" \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters "host=$(tofu output -raw db_address),portNumber=5432,localPortNumber=15432"
CREATE EXTENSION IF NOT EXISTS vector;
CREATE ROLE vocion_app LOGIN PASSWORD '<generated>';
ALTER DATABASE vocion OWNER TO vocion_app;

Store that login in rds_app_secret_name as {"username":"vocion_app","password":"<generated>"}.

3. Deploy. Run tofu output -raw deploy_command. The deploy checks out the pinned release, builds .env.production from the secret and app_env, builds the image on the host (or pulls one with VOCION_APP_IMAGE), applies migrations before swapping containers, starts the app behind Caddy, and fails loudly unless the new container reports the pinned commit.

4. First admin. The installation starts closed: no accounts, no users, and web sign-up only accepts invites. From a vocion-core checkout at the same release, with the port-forward from step 2 open, point DATABASE_URL at the vocion_app login and create the first admin:

DATABASE_URL='postgresql://vocion_app:<generated>@localhost:15432/vocion?sslmode=require' \
  npm run user:create --workspace @vocion/core -- \
  --email you@northwind.example --account "Northwind" --role admin

Everyone else is invited from Members.

Upgrade and roll back

  • Upgrade: set core_ref to the new tag, tofu apply, then deploy (or deploy with --parameters ref=v5.y.z first).
  • Roll back: deploy the previous tag. Migrations only move forward, written expand-then-contract so the previous release runs on the newer schema.
  • Change a secret: put-secret-value, then deploy. Change app_env: tofu apply, then deploy.

See Release lines and upgrades.

Without the module

infra/aws in vocion-core is the older single-host path: one EC2 with Postgres, Langfuse and Caddy in Docker Compose (bootstrap.sh, update.sh <ref>). New installations should use the module.