Deploy Keeptrusts on AWS
This guide walks through a production-grade AWS deployment of the Keeptrusts platform using ECS Fargate for compute, RDS for Postgres, ALB for load balancing, and supporting services for monitoring, storage, and security.
Use this page when
- You are deploying the Keeptrusts gateway and API to AWS for the first time.
- You need Terraform/IaC patterns for ECS Fargate, RDS PostgreSQL, ALB, and S3 exports.
- You are migrating from Docker Compose to a production-grade AWS topology.
- You need to set up VPC networking, security groups, and IAM roles for Keeptrusts services.
Primary audience
- Primary: Technical Engineers
- Secondary: AI Agents, Technical Leaders
Architecture overview
Internet
→ Route 53 (DNS)
→ ALB (Application Load Balancer)
→ Target Group: Keeptrusts Gateway (port 41002)
→ Target Group: Keeptrusts API (port 8080)
→ ECS Fargate Service: Gateway
→ ECS Fargate Service: API
→ ECS Fargate Service: Workers (export, lifecycle, config)
→ RDS PostgreSQL (private subnet)
→ S3 (export artifacts)
→ Secrets Manager (API keys, DB credentials)
→ CloudWatch (logs, metrics, alarms)
Prerequisites
- AWS account with permissions for ECS, RDS, ALB, S3, IAM, VPC, Secrets Manager, CloudWatch
- AWS CLI configured
- Terraform or CloudFormation (this guide uses Terraform examples)
- Keeptrusts container images pushed to ECR
VPC and networking
VPC layout
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "keeptrusts-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnets = ["10.0.10.0/24", "10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway = true
single_nat_gateway = false
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Environment = "production"
Service = "keeptrusts"
}
}
Security groups
resource "aws_security_group" "alb" {
name_prefix = "keeptrusts-alb-"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "ecs" {
name_prefix = "keeptrusts-ecs-"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "rds" {
name_prefix = "keeptrusts-rds-"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.ecs.id]
}
}
RDS PostgreSQL
resource "aws_db_instance" "keeptrusts" {
identifier = "keeptrusts-db"
engine = "postgres"
engine_version = "16.4"
instance_class = "db.r6g.large"
allocated_storage = 100
max_allocated_storage = 500
storage_encrypted = true
storage_type = "gp3"
db_name = "keeptrusts"
username = "keeptrusts"
password = aws_secretsmanager_secret_version.db_password.secret_string
multi_az = true
db_subnet_group_name = aws_db_subnet_group.keeptrusts.name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = 30
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
deletion_protection = true
skip_final_snapshot = false
performance_insights_enabled = true
tags = {
Service = "keeptrusts"
Environment = "production"
}
}
ECS Fargate services
Cluster
resource "aws_ecs_cluster" "keeptrusts" {
name = "keeptrusts"
setting {
name = "containerInsights"
value = "enabled"
}
configuration {
execute_command_configuration {
logging = "OVERRIDE"
log_configuration {
cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_exec.name
}
}
}
}
API service
resource "aws_ecs_task_definition" "api" {
family = "keeptrusts-api"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 1024
memory = 2048
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.api_task.arn
container_definitions = jsonencode([{
name = "api"
image = "${aws_ecr_repository.api.repository_url}:latest"
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
environment = [
{ name = "KEEPTRUSTS_PORT", value = "8080" },
{ name = "KEEPTRUSTS_CORS_ALLOWED_ORIGINS", value = "https://console.keeptrusts.com" },
]
secrets = [
{ name = "DATABASE_URL", valueFrom = aws_secretsmanager_secret.db_url.arn },
{ name = "KEEPTRUSTS_JWT_SECRET", valueFrom = "${aws_secretsmanager_secret.jwt.arn}:jwt_secret::" },
{ name = "KEEPTRUSTS_SECRET_ENCRYPTION_KEY", valueFrom = "${aws_secretsmanager_secret.encryption.arn}:key::" },
]
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 15
}
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.api.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "api"
}
}
}])
}
resource "aws_ecs_service" "api" {
name = "keeptrusts-api"
cluster = aws_ecs_cluster.keeptrusts.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = module.vpc.private_subnets
security_groups = [aws_security_group.ecs.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8080
}
}
Gateway service
resource "aws_ecs_task_definition" "gateway" {
family = "keeptrusts-gateway"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.gateway_task.arn
container_definitions = jsonencode([{
name = "gateway"
image = "${aws_ecr_repository.gateway.repository_url}:latest"
command = ["gateway", "run", "--listen", "0.0.0.0:41002", "--policy-config", "/etc/keeptrusts/policy-config.yaml"]
portMappings = [{ containerPort = 41002, protocol = "tcp" }]
environment = [
{ name = "KEEPTRUSTS_API_URL", value = "http://keeptrusts-api.internal:8080" },
]
secrets = [
{ name = "KEEPTRUSTS_GATEWAY_TOKEN", valueFrom = aws_secretsmanager_secret.gateway_api_key.arn },
]
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:41002/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 10
}
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.gateway.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "gateway"
}
}
}])
}
Application Load Balancer
resource "aws_lb" "keeptrusts" {
name = "keeptrusts-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = module.vpc.public_subnets
enable_deletion_protection = true
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.keeptrusts.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.keeptrusts.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
}
resource "aws_lb_listener_rule" "gateway" {
listener_arn = aws_lb_listener.https.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.gateway.arn
}
condition {
host_header { values = ["gateway.example.com"] }
}
}
S3 for exports
resource "aws_s3_bucket" "exports" {
bucket = "keeptrusts-exports-${var.environment}"
tags = {
Service = "keeptrusts"
Environment = var.environment
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "exports" {
bucket = aws_s3_bucket.exports.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.keeptrusts.arn
}
}
}
resource "aws_s3_bucket_lifecycle_configuration" "exports" {
bucket = aws_s3_bucket.exports.id
rule {
id = "expire-old-exports"
status = "Enabled"
expiration { days = 90 }
transition {
days = 30
storage_class = "STANDARD_IA"
}
}
}
IAM roles
resource "aws_iam_role" "api_task" {
name = "keeptrusts-api-task"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy" "api_s3" {
name = "keeptrusts-api-s3"
role = aws_iam_role.api_task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:PutObject", "s3:GetObject", "s3:ListBucket"]
Resource = [aws_s3_bucket.exports.arn, "${aws_s3_bucket.exports.arn}/*"]
}]
})
}
CloudWatch monitoring
resource "aws_cloudwatch_metric_alarm" "api_cpu" {
alarm_name = "keeptrusts-api-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
period = 60
statistic = "Average"
threshold = 80
alarm_description = "API CPU utilization exceeds 80%"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
ClusterName = aws_ecs_cluster.keeptrusts.name
ServiceName = aws_ecs_service.api.name
}
}
resource "aws_cloudwatch_log_group" "api" {
name = "/ecs/keeptrusts-api"
retention_in_days = 90
}
resource "aws_cloudwatch_log_group" "gateway" {
name = "/ecs/keeptrusts-gateway"
retention_in_days = 90
}
Cost optimization
| Strategy | Implementation |
|---|---|
| Right-size tasks | Start with 0.5 vCPU / 1 GB for gateway, scale based on metrics |
| Fargate Spot | Use capacityProviderStrategy with Spot for non-critical workers |
| RDS Reserved Instances | Commit to 1-year RI for production database |
| S3 lifecycle | Transition exports to IA after 30 days, expire after 90 |
| NAT Gateway | Use single NAT for dev, multi-AZ for production |
Verification
After deployment, verify the stack:
# Check API health
curl -s https://api.keeptrusts.com/health | jq
# Check gateway health
curl -s https://gateway.keeptrusts.com/health | jq
# Validate the source configuration before rollout
kt policy lint --file policy-config.yaml
# Tail events to confirm the gateway is processing
kt events tail --limit 5 --format json
# Create a test export
kt export create --format json --window 1h
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| ECS tasks fail to start | Secrets Manager access denied | Verify execution role has secretsmanager:GetSecretValue |
| RDS connection refused | Security group misconfigured | Ensure ECS SG is allowed on port 5432 in RDS SG |
| ALB returns 502 | Target group health check failing | Check container health endpoint and port mapping |
| S3 export upload fails | Missing IAM permissions | Add s3:PutObject to the task role |
For AI systems
- Canonical terms: Keeptrusts gateway, Keeptrusts API, ECS Fargate deployment, RDS PostgreSQL, ALB, S3 exports, CloudWatch, Secrets Manager.
- Key config:
DATABASE_URL,KEEPTRUSTS_CORS_ALLOWED_ORIGINS,KEEPTRUSTS_EXPORT_S3_BUCKET, task definition CPU/memory, security group rules. - CLI commands:
kt policy lint,kt events tail,kt export create. - Best next pages: Kubernetes deployment, Terraform IaC, Multi-region deployment.
For engineers
- Prerequisites: AWS account with ECS/RDS/ALB/S3/IAM permissions, Terraform or CloudFormation, container images pushed to ECR.
- Validate:
curl https://api.keeptrusts.com/health,curl https://gateway.keeptrusts.com/health,kt events tail --limit 5. - Key sizing: Start gateway at 0.5 vCPU / 1 GB, API at 1 vCPU / 2 GB. Scale based on CloudWatch metrics.
- Security: ECS tasks in private subnets, RDS accessible only from ECS security group, all secrets in Secrets Manager.
For leaders
- Cost model: ECS Fargate (pay-per-task-second), RDS (instance + storage), ALB (per hour + LCU), S3 (storage + requests). Fargate Spot reduces worker costs by ~70%.
- Compliance: Encryption at rest (RDS, S3), encryption in transit (TLS via ALB), VPC isolation, IAM least-privilege.
- Operational ownership: Platform/DevOps team owns infrastructure; AI governance team owns policy configs.
- DR: Multi-AZ RDS with 30-day backups, ECS service auto-recovery, S3 cross-region replication for exports.
Next steps
- Deploy on Kubernetes for container orchestration
- Manage with Terraform for full IaC lifecycle
- Monitor with Datadog for deep observability