LibreChat/deploy/aws-sam/template.yaml

1401 lines
No EOL
46 KiB
YAML

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Scalable LibreChat deployment on AWS using ECS Fargate'
Parameters:
Environment:
Type: String
Default: prod
AllowedValues: [dev, staging, prod]
Description: Environment name
VpcId:
Type: AWS::EC2::VPC::Id
Description: Existing VPC ID to deploy into
PublicSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: List of existing public subnet IDs for the load balancer (minimum 2 in different AZs)
PrivateSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: List of existing private subnet IDs for ECS tasks and databases (minimum 2 in different AZs)
LibreChatImage:
Type: String
Default: librechat/librechat:latest
Description: LibreChat Docker image custom build
DomainName:
Type: String
Default: ""
Description: Domain name for LibreChat (optional)
CertificateArn:
Type: String
Default: ""
Description: ACM certificate ARN for HTTPS (optional)
BedrockAccessKeyId:
Type: String
NoEcho: true
Default: ""
Description: "(Deprecated) AWS Access Key ID for Bedrock; use BedrockCredentialsSecretArn instead"
BedrockSecretAccessKey:
Type: String
NoEcho: true
Default: ""
Description: "(Deprecated) AWS Secret Access Key for Bedrock; use BedrockCredentialsSecretArn instead"
BedrockCredentialsSecretArn:
Type: String
Default: ""
Description: Secrets Manager ARN for Bedrock credentials JSON (accessKeyId, secretAccessKey); preferred over BedrockAccessKeyId/BedrockSecretAccessKey
EnableSSO:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Description: Enable SSO authentication with Cognito
CognitoUserPoolId:
Type: String
Default: ""
Description: Cognito User Pool ID for SSO (required if EnableSSO is true)
OpenIdClientId:
Type: String
Default: ""
Description: OpenID Client ID from Cognito App Client (required if EnableSSO is true)
OpenIdClientSecret:
Type: String
Default: ""
NoEcho: true
Description: "(Deprecated) OpenID Client Secret; use OpenIdClientSecretArn instead when EnableSSO is true"
OpenIdClientSecretArn:
Type: String
Default: ""
Description: Secrets Manager ARN for OpenID client secret string (preferred when EnableSSO is true)
OpenIdScope:
Type: String
Default: "openid profile email"
Description: OpenID scope for authentication
OpenIdButtonLabel:
Type: String
Default: "Sign in with SSO"
Description: Label for the SSO login button
OpenIdImageUrl:
Type: String
Default: ""
Description: Image URL for the SSO login button (optional)
OpenIdNameClaim:
Type: String
Default: "name"
Description: Claim attribute for user name
OpenIdEmailClaim:
Type: String
Default: "email"
Description: Claim attribute for user email
HelpAndFaqUrl:
Type: String
Default: ""
Description: Help and FAQ URL (use '/' to disable button)
CreateNATGateway:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Description: Create NAT Gateways for internet connectivity in private subnets
CreateSecretsManagerVPCEndpoint:
Type: String
Default: "true"
AllowedValues: ["true", "false"]
Description: Create Secrets Manager VPC endpoint in this stack. Set to false when deploying multiple stacks (dev/staging/prod) in the same VPC so the first stack's endpoint (with private DNS) is reused.
ExistingSecretsManagerEndpointSecurityGroupId:
Type: String
Default: ""
Description: When CreateSecretsManagerVPCEndpoint is false, the security group ID of the existing Secrets Manager VPC endpoint (so this stack's ECS tasks can be allowed to reach it). The deploy script sets this automatically.
# Optional MCP secrets (one secret per MCP; created by deploy script)
MCPCongressSecretArn:
Type: String
Default: ""
Description: Secrets Manager ARN for MCP Congress token (optional)
MCPEasternTimeSecretArn:
Type: String
Default: ""
Description: Secrets Manager ARN for MCP Eastern Time token (optional)
MCPPennkeyLookupSecretArn:
Type: String
Default: ""
Description: Secrets Manager ARN for MCP Pennkey Lookup token (optional)
Conditions:
HasDomain: !Not [!Equals [!Ref DomainName, ""]]
HasCertificate: !Not [!Equals [!Ref CertificateArn, ""]]
EnableSSO: !Equals [!Ref EnableSSO, "true"]
HasHelpUrl: !Not [!Equals [!Ref HelpAndFaqUrl, ""]]
CreateNATGateway: !Equals [!Ref CreateNATGateway, "true"]
CreateSecretsManagerVPCEndpoint: !Equals [!Ref CreateSecretsManagerVPCEndpoint, "true"]
HasMCPCongressSecret: !Not [!Equals [!Ref MCPCongressSecretArn, ""]]
HasMCPEasternTimeSecret: !Not [!Equals [!Ref MCPEasternTimeSecretArn, ""]]
HasMCPPennkeyLookupSecret: !Not [!Equals [!Ref MCPPennkeyLookupSecretArn, ""]]
UseBedrockCredentialsSecret: !Not [!Equals [!Ref BedrockCredentialsSecretArn, ""]]
UseOpenIdClientSecretArn: !And [!Equals [!Ref EnableSSO, "true"], !Not [!Equals [!Ref OpenIdClientSecretArn, ""]]]
Globals:
Function:
Timeout: 30
MemorySize: 512
Runtime: python3.11
Resources:
# NAT Gateway Resources (Conditional)
NATGateway1EIP:
Type: AWS::EC2::EIP
Condition: CreateNATGateway
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-nat-eip-1
NATGateway2EIP:
Type: AWS::EC2::EIP
Condition: CreateNATGateway
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-nat-eip-2
NATGateway1:
Type: AWS::EC2::NatGateway
Condition: CreateNATGateway
Properties:
AllocationId: !GetAtt NATGateway1EIP.AllocationId
SubnetId: !Select [0, !Ref PublicSubnetIds]
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-nat-gateway-1
NATGateway2:
Type: AWS::EC2::NatGateway
Condition: CreateNATGateway
Properties:
AllocationId: !GetAtt NATGateway2EIP.AllocationId
SubnetId: !Select [1, !Ref PublicSubnetIds]
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-nat-gateway-2
# Route Tables for Private Subnets (Conditional)
PrivateRouteTable1:
Type: AWS::EC2::RouteTable
Condition: CreateNATGateway
Properties:
VpcId: !Ref VpcId
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-rt-1
PrivateRouteTable2:
Type: AWS::EC2::RouteTable
Condition: CreateNATGateway
Properties:
VpcId: !Ref VpcId
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-rt-2
# Default Routes to NAT Gateways
PrivateRoute1:
Type: AWS::EC2::Route
Condition: CreateNATGateway
Properties:
RouteTableId: !Ref PrivateRouteTable1
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway1
PrivateRoute2:
Type: AWS::EC2::Route
Condition: CreateNATGateway
Properties:
RouteTableId: !Ref PrivateRouteTable2
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway2
# Associate Route Tables with Private Subnets
PrivateSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Condition: CreateNATGateway
Properties:
SubnetId: !Select [0, !Ref PrivateSubnetIds]
RouteTableId: !Ref PrivateRouteTable1
PrivateSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Condition: CreateNATGateway
Properties:
SubnetId: !Select [1, !Ref PrivateSubnetIds]
RouteTableId: !Ref PrivateRouteTable2
# Security Groups
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Application Load Balancer
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-alb-sg
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for ECS tasks
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3080
ToPort: 3080
SourceSecurityGroupId: !Ref ALBSecurityGroup
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-ecs-sg
DatabaseSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for databases
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 27017
ToPort: 27017
SourceSecurityGroupId: !Ref ECSSecurityGroup
- IpProtocol: tcp
FromPort: 6379
ToPort: 6379
SourceSecurityGroupId: !Ref ECSSecurityGroup
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-db-sg
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Lambda functions
VpcId: !Ref VpcId
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 2049
ToPort: 2049
CidrIp: 10.0.0.0/8
Description: NFS access to EFS (private subnets)
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS access for S3 API calls
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-lambda-sg
EFSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for EFS file system
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 2049
ToPort: 2049
SourceSecurityGroupId: !Ref ECSSecurityGroup
Description: NFS access from ECS tasks
- IpProtocol: tcp
FromPort: 2049
ToPort: 2049
SourceSecurityGroupId: !Ref LambdaSecurityGroup
Description: NFS access from Lambda functions
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-efs-sg
# VPC endpoint security group for Secrets Manager (so ECS tasks in private subnet can inject secrets)
# Only created when this stack owns the Secrets Manager VPC endpoint (first stack in VPC).
SecretsManagerEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Condition: CreateSecretsManagerVPCEndpoint
Properties:
GroupDescription: Security group for Secrets Manager VPC endpoint
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref ECSSecurityGroup
Description: HTTPS from ECS tasks for secret injection
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-secretsmanager-endpoint-sg
# VPC interface endpoint for Secrets Manager (required for ECS secret injection in private subnets).
# Only one endpoint per VPC can have private DNS; set CreateSecretsManagerVPCEndpoint=false for additional stacks in the same VPC.
SecretsManagerVPCEndpoint:
Type: AWS::EC2::VPCEndpoint
Condition: CreateSecretsManagerVPCEndpoint
Properties:
VpcId: !Ref VpcId
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.secretsmanager'
VpcEndpointType: Interface
PrivateDnsEnabled: true
SubnetIds: !Ref PrivateSubnetIds
SecurityGroupIds:
- !Ref SecretsManagerEndpointSecurityGroup
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-secretsmanager-endpoint
# When reusing an existing Secrets Manager VPC endpoint, add this stack's ECS SG to its SG before ECS Service starts
SecretsManagerEndpointEcsAccessRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: EC2SecurityGroupIngress
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ec2:AuthorizeSecurityGroupIngress
- ec2:RevokeSecurityGroupIngress
- ec2:DescribeSecurityGroups
Resource: "*"
SecretsManagerEndpointEcsAccessFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ${AWS::StackName}-secretsmanager-endpoint-ecs-access
CodeUri: src/secretsmanager_endpoint_ecs_access/
Handler: app.lambda_handler
Runtime: python3.11
Timeout: 60
MemorySize: 128
Role: !GetAtt SecretsManagerEndpointEcsAccessRole.Arn
SecretsManagerEndpointEcsAccessPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref SecretsManagerEndpointEcsAccessFunction
Action: lambda:InvokeFunction
Principal: cloudformation.amazonaws.com
SourceAccount: !Ref AWS::AccountId
SecretsManagerEndpointEcsAccess:
Type: AWS::CloudFormation::CustomResource
DependsOn: ECSSecurityGroup
Properties:
ServiceToken: !GetAtt SecretsManagerEndpointEcsAccessFunction.Arn
EndpointSecurityGroupId: !Ref ExistingSecretsManagerEndpointSecurityGroupId
EcsSecurityGroupId: !Ref ECSSecurityGroup
# Application Load Balancer
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub ${AWS::StackName}-alb
Scheme: internet-facing
Type: application
Subnets: !Ref PublicSubnetIds
SecurityGroups:
- !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-alb
ALBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub ${AWS::StackName}-tg
Port: 3080
Protocol: HTTP
VpcId: !Ref VpcId
TargetType: ip
HealthCheckPath: /health
HealthCheckProtocol: HTTP
HealthCheckIntervalSeconds: 60
HealthCheckTimeoutSeconds: 30
HealthyThresholdCount: 2
UnhealthyThresholdCount: 6
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ALBTargetGroup
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: !If [HasCertificate, 443, 80]
Protocol: !If [HasCertificate, HTTPS, HTTP]
Certificates: !If
- HasCertificate
- - CertificateArn: !Ref CertificateArn
- !Ref AWS::NoValue
# ECS Cluster
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ${AWS::StackName}-cluster
CapacityProviders:
- FARGATE
- FARGATE_SPOT
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 1
- CapacityProvider: FARGATE_SPOT
Weight: 4
ClusterSettings:
- Name: containerInsights
Value: enabled
# ECS Task Definition
ECSTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub ${AWS::StackName}-task
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: 1024
Memory: 2048
ExecutionRoleArn: !Ref ECSTaskExecutionRole
TaskRoleArn: !Ref ECSTaskRole
Volumes:
- Name: config-volume
EFSVolumeConfiguration:
FilesystemId: !Ref EFSFileSystem
RootDirectory: /lambda
TransitEncryption: ENABLED
AuthorizationConfig:
IAM: ENABLED
ContainerDefinitions:
- Name: librechat
Image: !Ref LibreChatImage
PortMappings:
- ContainerPort: 3080
Protocol: tcp
MountPoints:
- SourceVolume: config-volume
ContainerPath: /app/config
ReadOnly: true
Environment:
- Name: NODE_ENV
Value: production
- Name: MONGO_URI
Value: !Sub
- mongodb://librechat:{{resolve:secretsmanager:${DocumentDBPassword}:SecretString:password}}@${MongoHost}:27017/LibreChat?ssl=true&tlsAllowInvalidCertificates=true&authSource=admin&retryWrites=false
- MongoHost: !GetAtt DocumentDBCluster.Endpoint
DocumentDBPassword: !Ref DocumentDBPassword
- Name: USE_REDIS
Value: "true"
- Name: REDIS_URI
Value: !Sub
- rediss://${RedisHost}:6379 # Changed from redis:// to rediss://
- RedisHost: !GetAtt ElastiCacheReplicationGroup.PrimaryEndPoint.Address
# For ElastiCache with TLS
- Name: REDIS_USE_ALTERNATIVE_DNS_LOOKUP
Value: "true"
- Name: REDIS_KEY_PREFIX_VAR
Value: "AWS_EXECUTION_ENV" # or use static prefix
- Name: NODE_TLS_REJECT_UNAUTHORIZED
Value: "1"
# AWS Configuration (uses credentials from Secrets Manager)
- Name: AWS_REGION
Value: !Ref AWS::Region
# AWS Bedrock Configuration (keys in Secrets when BedrockCredentialsSecretArn set; else plain below)
- Name: BEDROCK_AWS_DEFAULT_REGION
Value: !Ref AWS::Region
- !If
- UseBedrockCredentialsSecret
- !Ref AWS::NoValue
- Name: BEDROCK_AWS_ACCESS_KEY_ID
Value: !Ref BedrockAccessKeyId
- !If
- UseBedrockCredentialsSecret
- !Ref AWS::NoValue
- Name: BEDROCK_AWS_SECRET_ACCESS_KEY
Value: !Ref BedrockSecretAccessKey
- Name: BEDROCK_AWS_MODELS
Value: "us.anthropic.claude-opus-4-20250514-v1:0,us.anthropic.claude-3-7-sonnet-20250219-v1:0,us.cohere.embed-v4:0,us.anthropic.claude-haiku-4-5-20251001-v1:0,us.anthropic.claude-sonnet-4-20250514-v1:0,us.anthropic.claude-opus-4-5-20251101-v1:0,us.anthropic.claude-sonnet-4-5-20250929-v1:0"
# Value: "us.anthropic.claude-opus-4-20250514-v1:0,us.anthropic.claude-3-7-sonnet-20250219-v1:0,us.cohere.embed-v4:0,us.anthropic.claude-haiku-4-5-20251001-v1:0,us.anthropic.claude-sonnet-4-20250514-v1:0,us.anthropic.claude-opus-4-5-20251101-v1:0,us.anthropic.claude-sonnet-4-5-20250929-v1:0,us.anthropic.claude-opus-4-1-20250805-v1:0"
# S3 Storage Credentials (in Secrets when Bedrock secret; else plain below)
- !If
- UseBedrockCredentialsSecret
- !Ref AWS::NoValue
- Name: AWS_ACCESS_KEY_ID
Value: !Ref BedrockAccessKeyId
- !If
- UseBedrockCredentialsSecret
- !Ref AWS::NoValue
- Name: AWS_SECRET_ACCESS_KEY
Value: !Ref BedrockSecretAccessKey
- Name: AWS_BUCKET_NAME
Value: !Sub ${AWS::StackName}-files-${AWS::AccountId}
# LibreChat config file path
- Name: CONFIG_PATH
Value: "/app/config/librechat.yaml"
#### APP CONFIG SETTINGS ####
# Allow new user registration
- Name: ALLOW_REGISTRATION
Value: "false"
# Help and Faq URL - If empty or commented, the button is enabled. To disable the Help and FAQ button, set to "/".
- Name: HELP_AND_FAQ_URL
Value: !If [HasHelpUrl, !Ref HelpAndFaqUrl, "https://www.google.edu"]
# Cache settings
- Name: CACHE
Value: "true"
- Name: ALLOW_SOCIAL_LOGIN
Value: !If [EnableSSO, "true", "false"]
- Name: ALLOW_SOCIAL_REGISTRATION
Value: !If [EnableSSO, "true", "false"]
- Name: DOMAIN_CLIENT
Value: !If [HasDomain, !Sub "https://${DomainName}", !Sub "https://${ApplicationLoadBalancer.DNSName}"]
- Name: DOMAIN_SERVER
Value: !If [HasDomain, !Sub "https://${DomainName}", !Sub "https://${ApplicationLoadBalancer.DNSName}"]
- Name: DEBUG_OPENID_REQUESTS
Value: "false"
- Name: DEBUG_OPENID
Value: "false"
- Name: DEBUG_DOMAIN_VALIDATION
Value: "false"
- Name: DEBUG_OAUTH_ERRORS
Value: "false"
- Name: DEBUG_SESSION_MESSAGES
Value: "false"
- Name: BYPASS_DOMAIN_CHECK
Value: "false"
# SSO Configuration (conditional)
- !If
- EnableSSO
- Name: OPENID_CLIENT_ID
Value: !Ref OpenIdClientId
- !Ref AWS::NoValue
# OPENID_CLIENT_SECRET in Secrets section when UseOpenIdClientSecretArn; else plain in Environment below
- !If
- EnableSSO
- Name: OPENID_ISSUER
Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPoolId}"
- !Ref AWS::NoValue
- !If
- EnableSSO
- Name: OPENID_SCOPE
Value: !Ref OpenIdScope
- !Ref AWS::NoValue
- !If
- EnableSSO
- Name: OPENID_CALLBACK_URL
Value: "/oauth/openid/callback"
- !Ref AWS::NoValue
- !If
- EnableSSO
- Name: OPENID_BUTTON_LABEL
Value: !Ref OpenIdButtonLabel
- !Ref AWS::NoValue
- !If
- EnableSSO
- Name: OPENID_IMAGE_URL
Value: !Ref OpenIdImageUrl
- !Ref AWS::NoValue
# OPENID_SESSION_SECRET in Secrets section
- !If
- EnableSSO
- Name: OPENID_GENERATE_NONCE
Value: "true"
- !Ref AWS::NoValue
- !If
- EnableSSO
- Name: OPENID_NAME_CLAIM
Value: !Ref OpenIdNameClaim
- !Ref AWS::NoValue
- !If
- EnableSSO
- Name: OPENID_EMAIL_CLAIM
Value: !Ref OpenIdEmailClaim
- !Ref AWS::NoValue
- Name: ALLOW_EMAIL_LOGIN
Value: !If [EnableSSO, "false", "true"]
# Session configuration to prevent conflicts
- Name: SESSION_EXPIRY
Value: "900000"
- Name: REFRESH_TOKEN_EXPIRY
Value: "604800000"
# Redis session configuration for multi-container deployments
- Name: SESSION_STORE
Value: "redis"
- Name: REDIS_SESSION_STORE
Value: "true"
# Debug (production-safe defaults)
- Name: DEBUG_LOGGING
Value: "false"
- Name: DEBUG_CONSOLE
Value: "false"
- Name: CONSOLE_JSON
Value: "true"
# OPENID_CLIENT_SECRET (plain only when not using secret ARN)
- !If
- EnableSSO
- !If
- UseOpenIdClientSecretArn
- !Ref AWS::NoValue
- Name: OPENID_CLIENT_SECRET
Value: !Ref OpenIdClientSecret
- !Ref AWS::NoValue
Secrets:
- Name: JWT_SECRET
ValueFrom: !Ref JWTSecret
- Name: JWT_REFRESH_SECRET
ValueFrom: !Ref JWTRefreshSecret
- Name: CREDS_KEY
ValueFrom: !Ref CredsKey
- Name: CREDS_IV
ValueFrom: !Ref CredsIV
- !If
- EnableSSO
- Name: OPENID_SESSION_SECRET
ValueFrom: !Ref OpenIdSessionSecret
- !Ref AWS::NoValue
- !If
- UseBedrockCredentialsSecret
- Name: BEDROCK_AWS_ACCESS_KEY_ID
ValueFrom: !Sub '${BedrockCredentialsSecretArn}:accessKeyId::'
- !Ref AWS::NoValue
- !If
- UseBedrockCredentialsSecret
- Name: BEDROCK_AWS_SECRET_ACCESS_KEY
ValueFrom: !Sub '${BedrockCredentialsSecretArn}:secretAccessKey::'
- !Ref AWS::NoValue
- !If
- UseBedrockCredentialsSecret
- Name: AWS_ACCESS_KEY_ID
ValueFrom: !Sub '${BedrockCredentialsSecretArn}:accessKeyId::'
- !Ref AWS::NoValue
- !If
- UseBedrockCredentialsSecret
- Name: AWS_SECRET_ACCESS_KEY
ValueFrom: !Sub '${BedrockCredentialsSecretArn}:secretAccessKey::'
- !Ref AWS::NoValue
- !If
- UseOpenIdClientSecretArn
- Name: OPENID_CLIENT_SECRET
ValueFrom: !Ref OpenIdClientSecretArn
- !Ref AWS::NoValue
- !If
- HasMCPCongressSecret
- Name: MCP_CONGRESS_TOKEN
ValueFrom: !Ref MCPCongressSecretArn
- !Ref AWS::NoValue
- !If
- HasMCPEasternTimeSecret
- Name: MCP_EASTERN_TIME_TOKEN
ValueFrom: !Ref MCPEasternTimeSecretArn
- !Ref AWS::NoValue
- !If
- HasMCPPennkeyLookupSecret
- Name: MCP_PENNKEY_LOOKUP_TOKEN
ValueFrom: !Ref MCPPennkeyLookupSecretArn
- !Ref AWS::NoValue
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref CloudWatchLogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: librechat
# ECS Service
ECSService:
Type: AWS::ECS::Service
DependsOn:
- ALBListener
- SecretsManagerEndpointEcsAccess
Properties:
ServiceName: !Sub ${AWS::StackName}-service
Cluster: !Ref ECSCluster
TaskDefinition: !Ref ECSTaskDefinition
LaunchType: FARGATE
DesiredCount: 2
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSecurityGroup
Subnets: !Ref PrivateSubnetIds
AssignPublicIp: DISABLED
LoadBalancers:
- ContainerName: librechat
ContainerPort: 3080
TargetGroupArn: !Ref ALBTargetGroup
HealthCheckGracePeriodSeconds: 300
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 50
DeploymentCircuitBreaker:
Enable: true
Rollback: true
EnableExecuteCommand: true
# Auto Scaling
ECSAutoScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: 20
MinCapacity: 1
ResourceId: !Sub service/${ECSCluster}/${ECSService.Name}
RoleARN: !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
ECSAutoScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: !Sub ${AWS::StackName}-scaling-policy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref ECSAutoScalingTarget
TargetTrackingScalingPolicyConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
TargetValue: 70.0
ScaleOutCooldown: 300
ScaleInCooldown: 300
# DocumentDB (MongoDB-compatible)
DocumentDBSubnetGroup:
Type: AWS::DocDB::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for DocumentDB
SubnetIds: !Ref PrivateSubnetIds
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-docdb-subnet-group
DocumentDBCluster:
Type: AWS::DocDB::DBCluster
Properties:
DBClusterIdentifier: !Sub ${AWS::StackName}-docdb
MasterUsername: librechat
MasterUserPassword: !Sub '{{resolve:secretsmanager:${DocumentDBPassword}:SecretString:password}}'
BackupRetentionPeriod: 7
PreferredBackupWindow: "03:00-04:00"
PreferredMaintenanceWindow: "sun:04:00-sun:05:00"
DBSubnetGroupName: !Ref DocumentDBSubnetGroup
VpcSecurityGroupIds:
- !Ref DatabaseSecurityGroup
StorageEncrypted: true
DeletionProtection: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-docdb
DocumentDBInstance1:
Type: AWS::DocDB::DBInstance
Properties:
DBClusterIdentifier: !Ref DocumentDBCluster
DBInstanceIdentifier: !Sub ${AWS::StackName}-docdb-1
DBInstanceClass: db.t3.medium
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-docdb-1
DocumentDBInstance2:
Type: AWS::DocDB::DBInstance
Properties:
DBClusterIdentifier: !Ref DocumentDBCluster
DBInstanceIdentifier: !Sub ${AWS::StackName}-docdb-2
DBInstanceClass: db.t3.medium
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-docdb-2
# ElastiCache Redis
ElastiCacheSubnetGroup:
Type: AWS::ElastiCache::SubnetGroup
Properties:
Description: Subnet group for ElastiCache
SubnetIds: !Ref PrivateSubnetIds
ElastiCacheReplicationGroup:
Type: AWS::ElastiCache::ReplicationGroup
Properties:
ReplicationGroupId: !Sub ${AWS::StackName}-redis
ReplicationGroupDescription: Redis cluster for LibreChat
CacheNodeType: cache.t3.micro
NumCacheClusters: 2
Engine: redis
EngineVersion: 7.0
Port: 6379
CacheSubnetGroupName: !Ref ElastiCacheSubnetGroup
SecurityGroupIds:
- !Ref DatabaseSecurityGroup
AtRestEncryptionEnabled: true
TransitEncryptionEnabled: true
MultiAZEnabled: true
AutomaticFailoverEnabled: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-redis
# EFS File System for Configuration Files
EFSFileSystem:
Type: AWS::EFS::FileSystem
Properties:
Encrypted: true
PerformanceMode: generalPurpose
ThroughputMode: bursting
FileSystemTags:
- Key: Name
Value: !Sub ${AWS::StackName}-config-efs
EFSMountTarget1:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref EFSFileSystem
SubnetId: !Select [0, !Ref PrivateSubnetIds]
SecurityGroups:
- !Ref EFSSecurityGroup
EFSMountTarget2:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref EFSFileSystem
SubnetId: !Select [1, !Ref PrivateSubnetIds]
SecurityGroups:
- !Ref EFSSecurityGroup
# EFS Access Point for Lambda function
EFSAccessPoint:
Type: AWS::EFS::AccessPoint
Properties:
FileSystemId: !Ref EFSFileSystem
PosixUser:
Uid: 1000
Gid: 1000
RootDirectory:
Path: /lambda
CreationInfo:
OwnerUid: 1000
OwnerGid: 1000
Permissions: 755
AccessPointTags:
- Key: Name
Value: !Sub ${AWS::StackName}-lambda-access-point
# Secrets Manager
DocumentDBPassword:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${AWS::StackName}/docdb/password
Description: DocumentDB master password
GenerateSecretString:
SecretStringTemplate: '{"username": "librechat"}'
GenerateStringKey: 'password'
PasswordLength: 32
ExcludeCharacters: '"@/\ ''`~!#$%^&*()+={}[]|:;<>?,'
JWTSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${AWS::StackName}/jwt/secret
Description: JWT secret for LibreChat
GenerateSecretString:
PasswordLength: 64
ExcludeCharacters: '"@/\'
JWTRefreshSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${AWS::StackName}/jwt/refresh-secret
Description: JWT refresh secret for LibreChat
GenerateSecretString:
PasswordLength: 64
ExcludeCharacters: '"@/\'
CredsKey:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${AWS::StackName}/creds/key
Description: Credentials encryption key for LibreChat
GenerateSecretString:
PasswordLength: 32
ExcludeCharacters: '"@/\'
CredsIV:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub ${AWS::StackName}/creds/iv
Description: Credentials encryption IV for LibreChat
GenerateSecretString:
PasswordLength: 16
ExcludeCharacters: '"@/\'
OpenIdSessionSecret:
Type: AWS::SecretsManager::Secret
Condition: EnableSSO
Properties:
Name: !Sub ${AWS::StackName}/openid/session-secret
Description: OpenID session secret for LibreChat SSO
GenerateSecretString:
PasswordLength: 64
ExcludeCharacters: '"@/\'
# IAM Roles
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Policies:
- PolicyName: SecretsManagerAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Ref DocumentDBPassword
- !Ref JWTSecret
- !Ref JWTRefreshSecret
- !Ref CredsKey
- !Ref CredsIV
- !If [HasMCPCongressSecret, !Ref MCPCongressSecretArn, !Ref AWS::NoValue]
- !If [HasMCPEasternTimeSecret, !Ref MCPEasternTimeSecretArn, !Ref AWS::NoValue]
- !If [HasMCPPennkeyLookupSecret, !Ref MCPPennkeyLookupSecretArn, !Ref AWS::NoValue]
- !If [UseBedrockCredentialsSecret, !Ref BedrockCredentialsSecretArn, !Ref AWS::NoValue]
- !If [UseOpenIdClientSecretArn, !Ref OpenIdClientSecretArn, !Ref AWS::NoValue]
- !If
- EnableSSO
- PolicyName: SSOSecretsManagerAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Ref OpenIdSessionSecret
- !Ref AWS::NoValue
ECSTaskRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: S3Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: !Sub
- "${BucketArn}/*"
- BucketArn: !GetAtt S3Bucket.Arn
- Effect: Allow
Action:
- s3:ListBucket
Resource: !GetAtt S3Bucket.Arn
- PolicyName: BedrockAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- bedrock:InvokeModel
- bedrock:ListFoundationModels
- bedrock:GetFoundationModel
Resource: "*"
- PolicyName: EFSAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- elasticfilesystem:ClientMount
- elasticfilesystem:ClientWrite
Resource: !Sub
- "arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${FileSystemId}"
- FileSystemId: !Ref EFSFileSystem
# Mount Target Waiter Lambda Role
MountTargetWaiterLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: EFSDescribeAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- elasticfilesystem:DescribeMountTargets
Resource: "*"
# Config Manager Lambda Role
ConfigManagerLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
Policies:
- PolicyName: S3ConfigAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
Resource: !Sub
- "${BucketArn}/configs/*"
- BucketArn: !GetAtt S3Bucket.Arn
- Effect: Allow
Action:
- s3:ListBucket
Resource: !GetAtt S3Bucket.Arn
Condition:
StringLike:
s3:prefix: "configs/*"
- PolicyName: EFSAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- elasticfilesystem:ClientMount
- elasticfilesystem:ClientWrite
Resource: !Sub
- "arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${FileSystemId}"
- FileSystemId: !Ref EFSFileSystem
# Lambda permission for CloudFormation to invoke Config Manager
ConfigManagerLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref ConfigManagerFunction
Action: lambda:InvokeFunction
Principal: cloudformation.amazonaws.com
SourceAccount: !Ref AWS::AccountId
# S3 Bucket for file storage and configuration
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${AWS::StackName}-files-${AWS::AccountId}
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: DeleteIncompleteMultipartUploads
Status: Enabled
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
# Mount Target Waiter Lambda Function
MountTargetWaiterFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ${AWS::StackName}-mount-target-waiter
CodeUri: src/mount_target_waiter/
Handler: app.lambda_handler
Runtime: python3.11
Timeout: 360
MemorySize: 256
Role: !GetAtt MountTargetWaiterLambdaRole.Arn
LoggingConfig:
LogGroup: !Ref MountTargetWaiterLogGroup
# Config Manager Lambda Function
ConfigManagerFunction:
Type: AWS::Serverless::Function
DependsOn:
- MountTargetWaiterTrigger
Properties:
FunctionName: !Sub ${AWS::StackName}-config-manager
CodeUri: src/config_manager/
Handler: app.lambda_handler
Runtime: python3.11
Timeout: 300
MemorySize: 512
Role: !GetAtt ConfigManagerLambdaRole.Arn
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds: !Ref PrivateSubnetIds
FileSystemConfigs:
- Arn: !Sub
- "arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${AccessPointId}"
- AccessPointId: !Ref EFSAccessPoint
LocalMountPath: /mnt/efs
Environment:
Variables:
EFS_MOUNT_PATH: /mnt/efs
S3_BUCKET_NAME: !Ref S3Bucket
Events:
# This function will be invoked by CloudFormation custom resource
# No direct events needed here
LoggingConfig:
LogGroup: !Ref ConfigManagerLogGroup
# CloudWatch Log Group for Mount Target Waiter Lambda
MountTargetWaiterLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${AWS::StackName}-mount-target-waiter
RetentionInDays: 30
# CloudWatch Log Group for Config Manager Lambda
ConfigManagerLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${AWS::StackName}-config-manager
RetentionInDays: 30
# Custom Resource to wait for mount targets to be ready
MountTargetWaiterTrigger:
Type: AWS::CloudFormation::CustomResource
DependsOn:
- EFSMountTarget1
- EFSMountTarget2
- EFSAccessPoint
Properties:
ServiceToken: !GetAtt MountTargetWaiterFunction.Arn
FileSystemId: !Ref EFSFileSystem
# Lambda permission for CloudFormation to invoke Mount Target Waiter
MountTargetWaiterLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref MountTargetWaiterFunction
Action: lambda:InvokeFunction
Principal: cloudformation.amazonaws.com
SourceAccount: !Ref AWS::AccountId
# Custom Resource to trigger Config Manager Lambda
ConfigManagerTrigger:
Type: AWS::CloudFormation::CustomResource
DependsOn:
- EFSMountTarget1
- EFSMountTarget2
- EFSAccessPoint
Properties:
ServiceToken: !GetAtt ConfigManagerFunction.Arn
S3Bucket: !Ref S3Bucket
S3Key: configs/librechat.yaml
EFSFileSystemId: !Ref EFSFileSystem
# Force update on every deployment by including timestamp-like value
DeploymentId: !Sub "${AWS::StackName}-${AWS::Region}-config"
# CloudWatch Log Group
CloudWatchLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /ecs/${AWS::StackName}
RetentionInDays: 30
Outputs:
LoadBalancerURL:
Description: URL of the Application Load Balancer
Value: !Sub
- ${Protocol}://${DNSName}
- Protocol: !If [HasCertificate, https, http]
DNSName: !GetAtt ApplicationLoadBalancer.DNSName
Export:
Name: !Sub ${AWS::StackName}-LoadBalancerURL
DocumentDBEndpoint:
Description: DocumentDB cluster endpoint
Value: !GetAtt DocumentDBCluster.Endpoint
Export:
Name: !Sub ${AWS::StackName}-DocumentDBEndpoint
RedisEndpoint:
Description: Redis cluster endpoint
Value: !GetAtt ElastiCacheReplicationGroup.PrimaryEndPoint.Address
Export:
Name: !Sub ${AWS::StackName}-RedisEndpoint
S3BucketName:
Description: S3 bucket for file storage
Value: !Ref S3Bucket
Export:
Name: !Sub ${AWS::StackName}-S3Bucket
ECSClusterName:
Description: ECS cluster name
Value: !Ref ECSCluster
Export:
Name: !Sub ${AWS::StackName}-ECSCluster
ECSServiceName:
Description: ECS service name
Value: !GetAtt ECSService.Name
Export:
Name: !Sub ${AWS::StackName}-ECSService
EFSFileSystemId:
Description: EFS file system ID for configuration files
Value: !Ref EFSFileSystem
Export:
Name: !Sub ${AWS::StackName}-EFSFileSystemId
EFSAccessPointId:
Description: EFS Access Point ID for Lambda function
Value: !Ref EFSAccessPoint
Export:
Name: !Sub ${AWS::StackName}-EFSAccessPointId
ConfigManagerFunctionArn:
Description: Config Manager Lambda function ARN
Value: !GetAtt ConfigManagerFunction.Arn
Export:
Name: !Sub ${AWS::StackName}-ConfigManagerFunctionArn
ConfigDeploymentStatus:
Description: Status of configuration file deployment to EFS
Value: !GetAtt ConfigManagerTrigger.FileSize
Export:
Name: !Sub ${AWS::StackName}-ConfigDeploymentStatus
SSOEnabled:
Description: Whether SSO authentication is enabled
Value: !Ref EnableSSO
Export:
Name: !Sub ${AWS::StackName}-SSOEnabled
CognitoIssuerURL:
Condition: EnableSSO
Description: Cognito OpenID Issuer URL
Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPoolId}"
Export:
Name: !Sub ${AWS::StackName}-CognitoIssuerURL
CallbackURL:
Condition: EnableSSO
Description: OAuth callback URL for Cognito configuration
Value: !Sub
- "${BaseURL}/oauth/openid/callback"
- BaseURL: !If
- HasDomain
- !Sub "https://${DomainName}"
- !Sub "https://${ApplicationLoadBalancer.DNSName}"
Export:
Name: !Sub ${AWS::StackName}-CallbackURL
NATGatewayEnabled:
Description: Whether NAT Gateways were created for internet connectivity
Value: !Ref CreateNATGateway
Export:
Name: !Sub ${AWS::StackName}-NATGatewayEnabled
NATGateway1Id:
Condition: CreateNATGateway
Description: NAT Gateway 1 ID
Value: !Ref NATGateway1
Export:
Name: !Sub ${AWS::StackName}-NATGateway1Id
NATGateway2Id:
Condition: CreateNATGateway
Description: NAT Gateway 2 ID
Value: !Ref NATGateway2
Export:
Name: !Sub ${AWS::StackName}-NATGateway2Id
NATGateway1EIP:
Condition: CreateNATGateway
Description: NAT Gateway 1 Elastic IP
Value: !Ref NATGateway1EIP
Export:
Name: !Sub ${AWS::StackName}-NATGateway1EIP
NATGateway2EIP:
Condition: CreateNATGateway
Description: NAT Gateway 2 Elastic IP
Value: !Ref NATGateway2EIP
Export:
Name: !Sub ${AWS::StackName}-NATGateway2EIP
foo:
Value: "bar bar"