Skip to main content

Amazon Web Services (AWS)

This guide deploys Contract Lucidity on AWS using managed services for high availability, auto-scaling, and minimal operational overhead.

Architecture

Prerequisites

  • AWS account with administrative permissions
  • AWS CLI v2 installed and configured (aws configure)
  • Docker installed locally (for building images)
  • A registered domain name

Step 1: VPC and Networking

Create a VPC with public and private subnets across two Availability Zones.

# Create VPC
aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=cl-vpc}]' \
--query 'Vpc.VpcId' --output text

# Enable DNS hostnames
aws ec2 modify-vpc-attribute --vpc-id <vpc-id> --enable-dns-hostnames

Create subnets:

SubnetCIDRAZPurpose
cl-public-110.0.1.0/24us-east-1aALB
cl-public-210.0.2.0/24us-east-1bALB
cl-private-110.0.10.0/24us-east-1aECS tasks
cl-private-210.0.11.0/24us-east-1bECS tasks
cl-data-110.0.20.0/24us-east-1aRDS, ElastiCache
cl-data-210.0.21.0/24us-east-1bRDS, ElastiCache

Create an Internet Gateway, NAT Gateway, and route tables so:

  • Public subnets route to the Internet Gateway
  • Private subnets route to the NAT Gateway (for pulling container images and accessing AWS APIs)
  • Data subnets have no internet access

Security Groups

Security GroupInbound RulesPurpose
cl-alb-sg80, 443 from 0.0.0.0/0Load balancer
cl-ecs-sg3000, 8000 from cl-alb-sgECS tasks
cl-rds-sg5432 from cl-ecs-sgPostgreSQL
cl-redis-sg6379 from cl-ecs-sgRedis
cl-efs-sg2049 from cl-ecs-sgEFS (if using EFS)

Step 2: RDS PostgreSQL with pgvector

# Create DB subnet group
aws rds create-db-subnet-group \
--db-subnet-group-name cl-db-subnets \
--db-subnet-group-description "CL data subnets" \
--subnet-ids <cl-data-1-id> <cl-data-2-id>

# Create the RDS instance
aws rds create-db-instance \
--db-instance-identifier cl-postgres \
--db-instance-class db.t3.medium \
--engine postgres \
--engine-version 16.5 \
--master-username cl_user \
--master-user-password '<strong-password>' \
--allocated-storage 50 \
--max-allocated-storage 200 \
--storage-type gp3 \
--db-name contract_lucidity \
--vpc-security-group-ids <cl-rds-sg-id> \
--db-subnet-group-name cl-db-subnets \
--multi-az \
--backup-retention-period 7 \
--storage-encrypted \
--no-publicly-accessible

After the instance is available, enable pgvector:

# Connect via psql (through a bastion or CloudShell in the VPC)
psql -h <rds-endpoint> -U cl_user -d contract_lucidity

# Inside psql
CREATE EXTENSION IF NOT EXISTS vector;
pgvector on RDS

Amazon RDS for PostgreSQL supports pgvector natively (version 0.8.0+ as of March 2026). No custom images or manual compilation required. The extension is available on PostgreSQL 16.5+.

Step 3: ElastiCache Redis

# Create cache subnet group
aws elasticache create-cache-subnet-group \
--cache-subnet-group-name cl-redis-subnets \
--cache-subnet-group-description "CL data subnets" \
--subnet-ids <cl-data-1-id> <cl-data-2-id>

# Create Redis cluster
aws elasticache create-replication-group \
--replication-group-id cl-redis \
--replication-group-description "Contract Lucidity Redis" \
--engine redis \
--engine-version 7.1 \
--cache-node-type cache.t3.small \
--num-cache-clusters 2 \
--cache-subnet-group-name cl-redis-subnets \
--security-group-ids <cl-redis-sg-id> \
--at-rest-encryption-enabled \
--transit-encryption-enabled \
--automatic-failover-enabled
Valkey Alternative

AWS now recommends Valkey (a Redis-compatible fork) for new deployments, priced 20% lower than Redis OSS on ElastiCache. Valkey is fully compatible with Contract Lucidity's Redis usage. To use Valkey, replace --engine redis with --engine valkey.

Step 4: Document Storage (EFS)

EFS provides a shared POSIX filesystem that multiple Fargate tasks can mount simultaneously.

# Create EFS filesystem
aws efs create-file-system \
--performance-mode generalPurpose \
--throughput-mode bursting \
--encrypted \
--tags Key=Name,Value=cl-storage \
--query 'FileSystemId' --output text

# Create mount targets in each private subnet
aws efs create-mount-target \
--file-system-id <efs-id> \
--subnet-id <cl-private-1-id> \
--security-groups <cl-efs-sg-id>

aws efs create-mount-target \
--file-system-id <efs-id> \
--subnet-id <cl-private-2-id> \
--security-groups <cl-efs-sg-id>

# Create an access point
aws efs create-access-point \
--file-system-id <efs-id> \
--posix-user Uid=1000,Gid=1000 \
--root-directory "Path=/data/storage,CreationInfo={OwnerUid=1000,OwnerGid=1000,Permissions=755}"
S3 Alternative

If you prefer object storage, you can use S3 with Mountpoint for Amazon S3 or modify the application to use the S3 API directly. EFS is simpler for the default POSIX filesystem approach. See Document Storage for a comparison.

Step 5: ECR Container Registry

# Create repositories
for repo in cl-backend cl-worker cl-frontend; do
aws ecr create-repository \
--repository-name contract-lucidity/$repo \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=AES256
done

# Login to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com

Build and push images:

cd contract-lucidity

# Backend
docker build -t cl-backend ./backend -f ./backend/Dockerfile
docker tag cl-backend:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-backend:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-backend:latest

# Worker
docker build -t cl-worker ./backend -f ./backend/Dockerfile.worker
docker tag cl-worker:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-worker:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-worker:latest

# Frontend
docker build -t cl-frontend ./frontend -f ./frontend/Dockerfile
docker tag cl-frontend:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-frontend:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-frontend:latest

Step 6: Secrets Manager

Store sensitive configuration in AWS Secrets Manager:

aws secretsmanager create-secret \
--name contract-lucidity/config \
--secret-string '{
"POSTGRES_USER": "cl_user",
"POSTGRES_PASSWORD": "<strong-password>",
"POSTGRES_DB": "contract_lucidity",
"POSTGRES_HOST": "<rds-endpoint>",
"POSTGRES_PORT": "5432",
"REDIS_URL": "rediss://cl-redis.xxxxx.cache.amazonaws.com:6379/0",
"CELERY_BROKER_URL": "rediss://cl-redis.xxxxx.cache.amazonaws.com:6379/0",
"CELERY_RESULT_BACKEND": "rediss://cl-redis.xxxxx.cache.amazonaws.com:6379/1",
"JWT_SECRET_KEY": "<generated-secret>",
"JWT_ALGORITHM": "HS256",
"JWT_ACCESS_TOKEN_EXPIRE_MINUTES": "60",
"JWT_REFRESH_TOKEN_EXPIRE_DAYS": "7",
"DEFAULT_ADMIN_EMAIL": "admin@your-domain.com",
"DEFAULT_ADMIN_PASSWORD": "<strong-password>",
"CORS_ORIGINS": "https://your-domain.com",
"FRONTEND_URL": "https://your-domain.com",
"STORAGE_PATH": "/data/storage"
}'
Redis SSL

When using ElastiCache with transit encryption enabled, use the rediss:// protocol (double s) in your Redis URLs.

Step 7: IAM Roles

Create two IAM roles:

ECS Task Execution Role (allows ECS to pull images and read secrets):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents",
"secretsmanager:GetSecretValue"
],
"Resource": "*"
}
]
}

ECS Task Role (allows running tasks to access EFS):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite"
],
"Resource": "<efs-arn>"
}
]
}

Step 8: ECS Fargate Task Definitions

Create an ECS cluster:

aws ecs create-cluster --cluster-name cl-cluster

cl-backend Task Definition

cl-backend-task.json
{
"family": "cl-backend",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "<execution-role-arn>",
"taskRoleArn": "<task-role-arn>",
"volumes": [
{
"name": "cl-storage",
"efsVolumeConfiguration": {
"fileSystemId": "<efs-id>",
"transitEncryption": "ENABLED",
"authorizationConfig": {
"accessPointId": "<access-point-id>",
"iam": "ENABLED"
}
}
}
],
"containerDefinitions": [
{
"name": "cl-backend",
"image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-backend:latest",
"portMappings": [{ "containerPort": 8000, "protocol": "tcp" }],
"mountPoints": [
{ "sourceVolume": "cl-storage", "containerPath": "/data/storage" }
],
"secrets": [
{ "name": "POSTGRES_USER", "valueFrom": "<secret-arn>:POSTGRES_USER::" },
{ "name": "POSTGRES_PASSWORD", "valueFrom": "<secret-arn>:POSTGRES_PASSWORD::" },
{ "name": "POSTGRES_DB", "valueFrom": "<secret-arn>:POSTGRES_DB::" },
{ "name": "POSTGRES_HOST", "valueFrom": "<secret-arn>:POSTGRES_HOST::" },
{ "name": "POSTGRES_PORT", "valueFrom": "<secret-arn>:POSTGRES_PORT::" },
{ "name": "REDIS_URL", "valueFrom": "<secret-arn>:REDIS_URL::" },
{ "name": "CELERY_BROKER_URL", "valueFrom": "<secret-arn>:CELERY_BROKER_URL::" },
{ "name": "CELERY_RESULT_BACKEND", "valueFrom": "<secret-arn>:CELERY_RESULT_BACKEND::" },
{ "name": "JWT_SECRET_KEY", "valueFrom": "<secret-arn>:JWT_SECRET_KEY::" },
{ "name": "JWT_ALGORITHM", "valueFrom": "<secret-arn>:JWT_ALGORITHM::" },
{ "name": "JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "valueFrom": "<secret-arn>:JWT_ACCESS_TOKEN_EXPIRE_MINUTES::" },
{ "name": "JWT_REFRESH_TOKEN_EXPIRE_DAYS", "valueFrom": "<secret-arn>:JWT_REFRESH_TOKEN_EXPIRE_DAYS::" },
{ "name": "DEFAULT_ADMIN_EMAIL", "valueFrom": "<secret-arn>:DEFAULT_ADMIN_EMAIL::" },
{ "name": "DEFAULT_ADMIN_PASSWORD", "valueFrom": "<secret-arn>:DEFAULT_ADMIN_PASSWORD::" },
{ "name": "CORS_ORIGINS", "valueFrom": "<secret-arn>:CORS_ORIGINS::" },
{ "name": "FRONTEND_URL", "valueFrom": "<secret-arn>:FRONTEND_URL::" },
{ "name": "STORAGE_PATH", "valueFrom": "<secret-arn>:STORAGE_PATH::" }
],
"environment": [
{ "name": "APP_ENV", "value": "production" },
{ "name": "LOG_LEVEL", "value": "INFO" },
{ "name": "MAX_UPLOAD_SIZE_MB", "value": "100" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/cl-backend",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}

cl-worker Task Definition

Same as cl-backend but with these changes:

cl-worker-task.json (differences only)
{
"family": "cl-worker",
"cpu": "2048",
"memory": "4096",
"containerDefinitions": [
{
"name": "cl-worker",
"image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-worker:latest",
"portMappings": [],
"environment": [
{ "name": "APP_ENV", "value": "production" },
{ "name": "LOG_LEVEL", "value": "INFO" },
{ "name": "CELERY_CONCURRENCY", "value": "4" }
]
}
]
}
Worker Sizing

The worker performs CPU-intensive OCR and AI processing. Allocate more vCPU and memory than the backend. The CELERY_CONCURRENCY value should not exceed the vCPU count to avoid resource contention.

cl-frontend Task Definition

cl-frontend-task.json (differences only)
{
"family": "cl-frontend",
"cpu": "1024",
"memory": "2048",
"containerDefinitions": [
{
"name": "cl-frontend",
"image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-frontend:latest",
"portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
"environment": [
{ "name": "BACKEND_INTERNAL_URL", "value": "http://cl-backend.cl-local:8000" },
{ "name": "NEXT_PUBLIC_FRONTEND_URL", "value": "https://your-domain.com" }
]
}
]
}

Register the task definitions:

aws ecs register-task-definition --cli-input-json file://cl-backend-task.json
aws ecs register-task-definition --cli-input-json file://cl-worker-task.json
aws ecs register-task-definition --cli-input-json file://cl-frontend-task.json

Step 9: ECS Services and Service Discovery

Create a Cloud Map namespace for internal service discovery:

aws servicediscovery create-private-dns-namespace \
--name cl-local \
--vpc <vpc-id>

Create ECS services:

# Backend service with service discovery
aws ecs create-service \
--cluster cl-cluster \
--service-name cl-backend \
--task-definition cl-backend \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<cl-private-1-id>,<cl-private-2-id>],securityGroups=[<cl-ecs-sg-id>],assignPublicIp=DISABLED}" \
--service-registries "registryArn=<service-discovery-arn>,containerName=cl-backend,containerPort=8000"

# Worker service (no load balancer needed)
aws ecs create-service \
--cluster cl-cluster \
--service-name cl-worker \
--task-definition cl-worker \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<cl-private-1-id>,<cl-private-2-id>],securityGroups=[<cl-ecs-sg-id>],assignPublicIp=DISABLED}"

# Frontend service
aws ecs create-service \
--cluster cl-cluster \
--service-name cl-frontend \
--task-definition cl-frontend \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<cl-private-1-id>,<cl-private-2-id>],securityGroups=[<cl-ecs-sg-id>],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=<target-group-arn>,containerName=cl-frontend,containerPort=3000"

Step 10: Application Load Balancer

# Create ALB
aws elbv2 create-load-balancer \
--name cl-alb \
--subnets <cl-public-1-id> <cl-public-2-id> \
--security-groups <cl-alb-sg-id> \
--scheme internet-facing \
--type application

# Create target group
aws elbv2 create-target-group \
--name cl-frontend-tg \
--protocol HTTP \
--port 3000 \
--vpc-id <vpc-id> \
--target-type ip \
--health-check-path "/" \
--health-check-interval-seconds 30

# Request SSL certificate
aws acm request-certificate \
--domain-name your-domain.com \
--validation-method DNS

# Create HTTPS listener (after certificate is validated)
aws elbv2 create-listener \
--load-balancer-arn <alb-arn> \
--protocol HTTPS \
--port 443 \
--certificates CertificateArn=<certificate-arn> \
--default-actions Type=forward,TargetGroupArn=<target-group-arn>

# Create HTTP → HTTPS redirect
aws elbv2 create-listener \
--load-balancer-arn <alb-arn> \
--protocol HTTP \
--port 80 \
--default-actions Type=redirect,RedirectConfig='{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}'

Step 11: DNS

Point your domain to the ALB:

# If using Route 53
aws route53 change-resource-record-sets \
--hosted-zone-id <zone-id> \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "your-domain.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "<alb-hosted-zone-id>",
"DNSName": "<alb-dns-name>",
"EvaluateTargetHealth": true
}
}
}]
}'

Cost Estimate

Estimated monthly costs (as of March 2026) for a production deployment in US East (N. Virginia):

ServiceSpecificationEstimated Monthly Cost
ECS Fargate (frontend)2 tasks x 1 vCPU / 2 GB~$60
ECS Fargate (backend)2 tasks x 1 vCPU / 2 GB~$60
ECS Fargate (worker)1 task x 2 vCPU / 4 GB~$60
RDS PostgreSQLdb.t3.medium, Multi-AZ, 50 GB gp3~$140
ElastiCache Rediscache.t3.small, 2 nodes~$50
EFS10 GB (scales automatically)~$3
ALB1 ALB + data processing~$25
ECRImage storage~$2
Secrets Manager1 secret~$1
Data TransferModerate usage~$10
CloudWatch LogsLog storage~$5
Total~$416/month
Cost Optimization
  • Use Fargate Spot for the worker (up to 70% savings for interruptible workloads). Document processing tasks are retried automatically.
  • Use Reserved Instances for RDS and ElastiCache (up to 55% savings with 1-year commitment).
  • Use Savings Plans for Fargate compute (up to 50% savings).
  • Use ARM/Graviton instances where possible for a 20% price reduction. The Python backend and Node.js frontend both run on ARM.

Auto-Scaling

Configure auto-scaling for the frontend and backend services:

# Register scalable target
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/cl-cluster/cl-frontend \
--min-capacity 2 \
--max-capacity 10

# Create scaling policy (target 70% CPU)
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/cl-cluster/cl-frontend \
--policy-name cl-frontend-cpu \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'

Verification

# Check ECS services
aws ecs describe-services --cluster cl-cluster \
--services cl-backend cl-worker cl-frontend \
--query 'services[].{name:serviceName,status:status,running:runningCount,desired:desiredCount}'

# Check ALB health
aws elbv2 describe-target-health --target-group-arn <target-group-arn>

# Test the application
curl -I https://your-domain.com
Pricing Disclaimer

Verify current pricing and service availability at aws.amazon.com/pricing. AWS pricing changes frequently and may vary by region.