mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
1009 lines
No EOL
35 KiB
Bash
1009 lines
No EOL
35 KiB
Bash
#!/bin/bash
|
|
|
|
# LibreChat AWS SAM Deployment Script
|
|
# Interactive deployment for existing VPC infrastructure with Transit Gateway
|
|
|
|
set -e
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
MAGENTA='\033[0;35m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Configuration file
|
|
CONFIG_FILE=".librechat-deploy-config"
|
|
|
|
# Default values
|
|
ENVIRONMENT="prod"
|
|
REGION="us-east-1"
|
|
STACK_NAME="librechat"
|
|
DOMAIN_NAME=""
|
|
CERTIFICATE_ARN=""
|
|
VPC_ID=""
|
|
PUBLIC_SUBNETS=""
|
|
PRIVATE_SUBNETS=""
|
|
AWS_ACCESS_KEY_ID=""
|
|
AWS_SECRET_ACCESS_KEY=""
|
|
ENABLE_SSO="false"
|
|
COGNITO_USER_POOL_ID=""
|
|
OPENID_CLIENT_ID=""
|
|
OPENID_CLIENT_SECRET=""
|
|
OPENID_SCOPE="openid profile email"
|
|
OPENID_BUTTON_LABEL="Sign in with SSO"
|
|
OPENID_IMAGE_URL=""
|
|
OPENID_NAME_CLAIM="name"
|
|
OPENID_EMAIL_CLAIM="email"
|
|
HELP_AND_FAQ_URL=""
|
|
CREATE_NAT_GATEWAY="false"
|
|
# Set to false when deploying staging/prod in same VPC as dev (dev already created Secrets Manager VPC endpoint with private DNS)
|
|
CREATE_SECRETS_MANAGER_VPC_ENDPOINT="true"
|
|
# Optional: set to override LibreChatImage (e.g. custom ECR image); when unset, template default is used
|
|
LIBRECHAT_IMAGE=""
|
|
# MCP secrets (optional; one per MCP server)
|
|
MCP_CONGRESS_TOKEN=""
|
|
MCP_EASTERN_TIME_TOKEN=""
|
|
|
|
# Function to generate random string
|
|
generate_random_string() {
|
|
local length="$1"
|
|
openssl rand -base64 "$length" | tr -d "=+/" | cut -c1-"$length"
|
|
}
|
|
|
|
# Function to print colored output
|
|
print_status() {
|
|
echo -e "${BLUE}[INFO]${NC} $1"
|
|
}
|
|
|
|
print_success() {
|
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
}
|
|
|
|
print_warning() {
|
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
}
|
|
|
|
print_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
print_prompt() {
|
|
echo -e "${CYAN}[INPUT]${NC} $1"
|
|
}
|
|
|
|
print_important() {
|
|
echo -e "${MAGENTA}[IMPORTANT]${NC} $1"
|
|
}
|
|
|
|
# Function to show usage
|
|
usage() {
|
|
echo "LibreChat AWS SAM Interactive Deployment"
|
|
echo ""
|
|
echo "Usage: $0 [OPTIONS]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --load-config Load configuration from saved file"
|
|
echo " --reset-config Delete saved configuration and start fresh"
|
|
echo " --update-config Update config file and restart containers (no full deployment)"
|
|
echo " -h, --help Show this help message"
|
|
echo ""
|
|
echo "The script will interactively prompt for all required parameters"
|
|
echo "and save them to '$CONFIG_FILE' for future deployments."
|
|
}
|
|
|
|
# Function to save configuration
|
|
save_config() {
|
|
cat > "$CONFIG_FILE" << EOF
|
|
# LibreChat Deployment Configuration
|
|
# Generated on $(date)
|
|
ENVIRONMENT="$ENVIRONMENT"
|
|
REGION="$REGION"
|
|
STACK_NAME="$STACK_NAME"
|
|
DOMAIN_NAME="$DOMAIN_NAME"
|
|
CERTIFICATE_ARN="$CERTIFICATE_ARN"
|
|
VPC_ID="$VPC_ID"
|
|
PUBLIC_SUBNETS="$PUBLIC_SUBNETS"
|
|
PRIVATE_SUBNETS="$PRIVATE_SUBNETS"
|
|
AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID"
|
|
AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY"
|
|
ENABLE_SSO="$ENABLE_SSO"
|
|
COGNITO_USER_POOL_ID="$COGNITO_USER_POOL_ID"
|
|
OPENID_CLIENT_ID="$OPENID_CLIENT_ID"
|
|
OPENID_CLIENT_SECRET="$OPENID_CLIENT_SECRET"
|
|
OPENID_SCOPE="$OPENID_SCOPE"
|
|
OPENID_BUTTON_LABEL="$OPENID_BUTTON_LABEL"
|
|
OPENID_IMAGE_URL="$OPENID_IMAGE_URL"
|
|
OPENID_NAME_CLAIM="$OPENID_NAME_CLAIM"
|
|
OPENID_EMAIL_CLAIM="$OPENID_EMAIL_CLAIM"
|
|
HELP_AND_FAQ_URL="$HELP_AND_FAQ_URL"
|
|
CREATE_NAT_GATEWAY="$CREATE_NAT_GATEWAY"
|
|
CREATE_SECRETS_MANAGER_VPC_ENDPOINT="$CREATE_SECRETS_MANAGER_VPC_ENDPOINT"
|
|
LIBRECHAT_IMAGE="$LIBRECHAT_IMAGE"
|
|
MCP_CONGRESS_TOKEN="$MCP_CONGRESS_TOKEN"
|
|
MCP_EASTERN_TIME_TOKEN="$MCP_EASTERN_TIME_TOKEN"
|
|
EOF
|
|
print_success "Configuration saved to $CONFIG_FILE"
|
|
}
|
|
|
|
# Function to load configuration
|
|
load_config() {
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
print_status "Loading configuration from $CONFIG_FILE"
|
|
source "$CONFIG_FILE"
|
|
print_success "Configuration loaded successfully"
|
|
return 0
|
|
else
|
|
print_warning "No configuration file found at $CONFIG_FILE"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to prompt for input with default value
|
|
prompt_input() {
|
|
local prompt="$1"
|
|
local default="$2"
|
|
local var_name="$3"
|
|
local current_value="${!var_name}"
|
|
|
|
if [[ -n "$current_value" ]]; then
|
|
default="$current_value"
|
|
fi
|
|
|
|
if [[ -n "$default" ]]; then
|
|
print_prompt "$prompt [$default]: "
|
|
read -r input
|
|
if [[ -z "$input" ]]; then
|
|
input="$default"
|
|
fi
|
|
else
|
|
print_prompt "$prompt: "
|
|
read -r input
|
|
while [[ -z "$input" ]]; do
|
|
print_error "This field is required!"
|
|
print_prompt "$prompt: "
|
|
read -r input
|
|
done
|
|
fi
|
|
|
|
eval "$var_name=\"$input\""
|
|
}
|
|
|
|
# Function to validate AWS CLI and credentials
|
|
validate_aws() {
|
|
if ! command -v aws &> /dev/null; then
|
|
print_error "AWS CLI is not installed. Please install it first."
|
|
exit 1
|
|
fi
|
|
|
|
if ! aws sts get-caller-identity &> /dev/null; then
|
|
print_error "AWS credentials not configured. Please run 'aws configure' first."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Function to ensure ECS service-linked role exists
|
|
ensure_ecs_service_role() {
|
|
local region="$1"
|
|
|
|
print_status "Checking ECS service-linked role..."
|
|
|
|
# Check if the service-linked role exists
|
|
if aws iam get-role --role-name AWSServiceRoleForECS --region "$region" &>/dev/null; then
|
|
print_success "ECS service-linked role already exists"
|
|
return 0
|
|
fi
|
|
|
|
print_status "Creating ECS service-linked role..."
|
|
|
|
# Create the service-linked role
|
|
if aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com --region "$region" &>/dev/null; then
|
|
print_success "ECS service-linked role created successfully"
|
|
|
|
# Wait a moment for the role to propagate
|
|
print_status "Waiting for role to propagate..."
|
|
sleep 10
|
|
|
|
return 0
|
|
else
|
|
# Check if it failed because the role already exists
|
|
if aws iam get-role --role-name AWSServiceRoleForECS --region "$region" &>/dev/null; then
|
|
print_success "ECS service-linked role already exists (created by another process)"
|
|
return 0
|
|
else
|
|
print_error "Failed to create ECS service-linked role"
|
|
print_error "Please create it manually with: aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com"
|
|
return 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Function to validate AWS Bedrock access
|
|
validate_bedrock_access() {
|
|
local access_key="$1"
|
|
local secret_key="$2"
|
|
local region="$3"
|
|
|
|
print_status "Validating AWS Bedrock access..."
|
|
|
|
# Test Bedrock access with provided credentials
|
|
AWS_ACCESS_KEY_ID="$access_key" AWS_SECRET_ACCESS_KEY="$secret_key" \
|
|
aws bedrock list-foundation-models --region "$region" &>/dev/null
|
|
|
|
if [[ $? -eq 0 ]]; then
|
|
print_success "AWS Bedrock access validated successfully"
|
|
return 0
|
|
else
|
|
print_warning "Could not validate Bedrock access. Please ensure:"
|
|
print_warning "1. The credentials have Bedrock permissions"
|
|
print_warning "2. Bedrock model access is enabled in your AWS account"
|
|
print_warning "3. The region supports Bedrock services"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to validate SAM CLI
|
|
validate_sam() {
|
|
if ! command -v sam &> /dev/null; then
|
|
print_error "SAM CLI is not installed. Please install it first."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Function to list VPCs
|
|
list_vpcs() {
|
|
print_status "Available VPCs in region $REGION:"
|
|
AWS_PAGER="" aws ec2 describe-vpcs \
|
|
--region "$REGION" \
|
|
--query 'Vpcs[*].[VpcId,CidrBlock,Tags[?Key==`Name`].Value|[0]]' \
|
|
--output table 2>/dev/null || print_warning "Could not list VPCs"
|
|
}
|
|
|
|
# Function to list subnets for a VPC
|
|
list_subnets() {
|
|
local vpc_id="$1"
|
|
if [[ -n "$vpc_id" ]]; then
|
|
print_status "Available subnets in VPC $vpc_id:"
|
|
AWS_PAGER="" aws ec2 describe-subnets \
|
|
--region "$REGION" \
|
|
--filters "Name=vpc-id,Values=$vpc_id" \
|
|
--query 'Subnets[*].[SubnetId,CidrBlock,AvailabilityZone,Tags[?Key==`Name`].Value|[0]]' \
|
|
--output table 2>/dev/null || print_warning "Could not list subnets"
|
|
fi
|
|
}
|
|
|
|
# Function to upload config to S3
|
|
upload_config() {
|
|
local stack_name="$1"
|
|
local region="$2"
|
|
|
|
# Get S3 bucket name from CloudFormation outputs
|
|
local bucket_name=$(aws cloudformation describe-stacks \
|
|
--stack-name "$stack_name" \
|
|
--region "$region" \
|
|
--query 'Stacks[0].Outputs[?OutputKey==`S3BucketName`].OutputValue' \
|
|
--output text 2>/dev/null)
|
|
|
|
if [[ -z "$bucket_name" ]]; then
|
|
print_warning "Could not find S3 bucket name from stack outputs"
|
|
return 1
|
|
fi
|
|
|
|
if [[ -f "librechat.yaml" ]]; then
|
|
print_status "Uploading librechat.yaml to S3..."
|
|
aws s3 cp librechat.yaml "s3://$bucket_name/configs/librechat.yaml" \
|
|
--content-type "application/x-yaml" \
|
|
--region "$region"
|
|
|
|
if [[ $? -eq 0 ]]; then
|
|
print_success "Configuration uploaded to s3://$bucket_name/configs/librechat.yaml"
|
|
return 0
|
|
else
|
|
print_error "Failed to upload configuration to S3"
|
|
return 1
|
|
fi
|
|
else
|
|
print_warning "librechat.yaml not found - skipping config upload"
|
|
print_status "A default configuration will be used"
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# Function to trigger config update and container restart
|
|
update_config() {
|
|
local stack_name="$1"
|
|
local region="$2"
|
|
|
|
print_status "Updating configuration and restarting containers..."
|
|
|
|
# Upload config to S3
|
|
if ! upload_config "$stack_name" "$region"; then
|
|
return 1
|
|
fi
|
|
|
|
# Trigger Config Manager Lambda to copy S3 → EFS
|
|
local lambda_name="${stack_name}-config-manager"
|
|
print_status "Triggering config manager Lambda: $lambda_name"
|
|
|
|
aws lambda invoke \
|
|
--function-name "$lambda_name" \
|
|
--region "$region" \
|
|
--payload '{}' \
|
|
/tmp/lambda-response.json >/dev/null 2>&1
|
|
|
|
if [[ $? -eq 0 ]]; then
|
|
print_success "Config manager Lambda executed successfully"
|
|
else
|
|
print_warning "Could not invoke config manager Lambda (may not exist yet)"
|
|
fi
|
|
|
|
# Force ECS service to restart containers
|
|
local cluster_name=$(aws cloudformation describe-stacks \
|
|
--stack-name "$stack_name" \
|
|
--region "$region" \
|
|
--query 'Stacks[0].Outputs[?OutputKey==`ECSClusterName`].OutputValue' \
|
|
--output text 2>/dev/null)
|
|
|
|
local service_name=$(aws cloudformation describe-stacks \
|
|
--stack-name "$stack_name" \
|
|
--region "$region" \
|
|
--query 'Stacks[0].Outputs[?OutputKey==`ECSServiceName`].OutputValue' \
|
|
--output text 2>/dev/null)
|
|
|
|
if [[ -n "$cluster_name" && -n "$service_name" ]]; then
|
|
print_status "Restarting ECS containers to pick up new config..."
|
|
aws ecs update-service \
|
|
--cluster "$cluster_name" \
|
|
--service "$service_name" \
|
|
--region "$region" \
|
|
--force-new-deployment >/dev/null 2>&1
|
|
|
|
if [[ $? -eq 0 ]]; then
|
|
print_success "ECS service restart initiated"
|
|
print_status "Containers will restart with the new configuration"
|
|
else
|
|
print_warning "Could not restart ECS service"
|
|
fi
|
|
else
|
|
print_warning "Could not find ECS cluster/service information"
|
|
fi
|
|
}
|
|
|
|
encode_certificate() {
|
|
local cert="$1"
|
|
# Remove any existing escaping and re-encode properly
|
|
echo "$cert" | sed 's/\\n/\n/g' | tr '\n' '\\n' | sed 's/\\n$//'
|
|
}
|
|
|
|
validate_subnets() {
|
|
local subnet_list="$1"
|
|
local subnet_type="$2"
|
|
|
|
if [[ -z "$subnet_list" ]]; then
|
|
print_error "$subnet_type subnets cannot be empty"
|
|
return 1
|
|
fi
|
|
|
|
# Convert comma-separated list to array
|
|
IFS=',' read -ra subnets <<< "$subnet_list"
|
|
|
|
if [[ ${#subnets[@]} -lt 2 ]]; then
|
|
print_error "$subnet_type subnets must include at least 2 subnets in different AZs"
|
|
return 1
|
|
fi
|
|
|
|
print_status "Validating $subnet_type subnets..."
|
|
for subnet in "${subnets[@]}"; do
|
|
subnet=$(echo "$subnet" | xargs) # trim whitespace
|
|
if ! aws ec2 describe-subnets --region "$REGION" --subnet-ids "$subnet" &>/dev/null; then
|
|
print_error "Subnet $subnet not found or not accessible"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
print_success "$subnet_type subnets validated successfully"
|
|
return 0
|
|
}
|
|
|
|
# Create or update a Secrets Manager secret with a string value; output the secret ARN
|
|
ensure_secret_string() {
|
|
local name="$1"
|
|
local value="$2"
|
|
local region="$3"
|
|
if aws secretsmanager describe-secret --secret-id "$name" --region "$region" &>/dev/null; then
|
|
aws secretsmanager put-secret-value --secret-id "$name" --secret-string "$value" --region "$region" >/dev/null
|
|
aws secretsmanager describe-secret --secret-id "$name" --region "$region" --query ARN --output text
|
|
else
|
|
aws secretsmanager create-secret --name "$name" --secret-string "$value" --region "$region" --query ARN --output text
|
|
fi
|
|
}
|
|
|
|
# Create or update Bedrock credentials secret (JSON with accessKeyId, secretAccessKey); output the secret ARN
|
|
ensure_bedrock_credentials_secret() {
|
|
local name="$1"
|
|
local access_key="$2"
|
|
local secret_key="$3"
|
|
local region="$4"
|
|
[[ -z "$access_key" || -z "$secret_key" ]] && { print_error "Bedrock credentials (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) are required."; exit 1; }
|
|
local json
|
|
# Pass AK/SK as env vars so Python can read them (AK="$x" SK="$y" command sets env for command only)
|
|
json=$(AK="$access_key" SK="$secret_key" python3 -c 'import json,os; print(json.dumps({"accessKeyId":os.environ.get("AK",""),"secretAccessKey":os.environ.get("SK","")}))')
|
|
ensure_secret_string "$name" "$json" "$region"
|
|
}
|
|
|
|
# Interactive configuration function
|
|
interactive_config() {
|
|
echo ""
|
|
echo "=============================================="
|
|
echo " LibreChat AWS SAM Interactive Deployment"
|
|
echo "=============================================="
|
|
echo ""
|
|
|
|
print_important "🌐 NETWORK ARCHITECTURE NOTICE 🌐"
|
|
print_important "This deployment supports two network connectivity options:"
|
|
print_important ""
|
|
print_important "Option 1: Transit Gateway (Recommended for existing infrastructure)"
|
|
print_important "✅ NO NAT GATEWAYS created (saves ~\$90/month)"
|
|
print_important "✅ Uses existing Transit Gateway for internet access"
|
|
print_important "✅ Private subnets remain secure with controlled routing"
|
|
print_important ""
|
|
print_important "Option 2: NAT Gateway (Standard AWS pattern)"
|
|
print_important "• Creates NAT Gateways in each AZ (~\$90/month cost)"
|
|
print_important "• Provides direct internet access for private subnets"
|
|
print_important "• Higher availability and performance guarantees"
|
|
print_important "• No dependency on existing Transit Gateway"
|
|
echo ""
|
|
|
|
# Environment
|
|
print_status "Step 1: Environment Configuration"
|
|
echo "Available environments: dev, staging, prod"
|
|
prompt_input "Environment" "$ENVIRONMENT" "ENVIRONMENT"
|
|
|
|
if [[ ! "$ENVIRONMENT" =~ ^(dev|staging|prod)$ ]]; then
|
|
print_error "Invalid environment: $ENVIRONMENT. Must be dev, staging, or prod."
|
|
exit 1
|
|
fi
|
|
|
|
# Region
|
|
prompt_input "AWS Region" "$REGION" "REGION"
|
|
|
|
# Stack name
|
|
prompt_input "CloudFormation Stack Name" "$STACK_NAME" "STACK_NAME"
|
|
|
|
echo ""
|
|
print_status "Step 2: Network Configuration"
|
|
|
|
# NAT Gateway option
|
|
print_status "Internet Connectivity Options:"
|
|
print_status "1. Transit Gateway (existing infrastructure) - No additional cost"
|
|
print_status "2. NAT Gateway (standard AWS pattern) - ~\$90/month"
|
|
echo ""
|
|
print_prompt "Create NAT Gateways for internet connectivity? (y/N): (IF YOU ALREADY HAVE A NAT GATEWAY, THE ANSWER IS NO.) "
|
|
read -r nat_choice
|
|
if [[ "$nat_choice" =~ ^[Yy]$ ]]; then
|
|
CREATE_NAT_GATEWAY="true"
|
|
print_success "NAT Gateways will be created for internet connectivity"
|
|
print_warning "This will add approximately \$90/month to your AWS bill"
|
|
else
|
|
CREATE_NAT_GATEWAY="false"
|
|
print_success "Using existing Transit Gateway infrastructure (no NAT Gateway cost)"
|
|
print_important "Ensure your private subnets have routes to Transit Gateway for internet access"
|
|
fi
|
|
|
|
# VPC ID
|
|
list_vpcs
|
|
echo ""
|
|
prompt_input "VPC ID" "$VPC_ID" "VPC_ID"
|
|
|
|
# Validate VPC
|
|
if ! aws ec2 describe-vpcs --region "$REGION" --vpc-ids "$VPC_ID" &>/dev/null; then
|
|
print_error "VPC $VPC_ID not found or not accessible"
|
|
exit 1
|
|
fi
|
|
|
|
# Public Subnets
|
|
echo ""
|
|
list_subnets "$VPC_ID"
|
|
echo ""
|
|
print_status "Public subnets will be used for the Application Load Balancer"
|
|
prompt_input "Public Subnet IDs (comma-separated, minimum 2)" "$PUBLIC_SUBNETS" "PUBLIC_SUBNETS"
|
|
|
|
if ! validate_subnets "$PUBLIC_SUBNETS" "Public"; then
|
|
exit 1
|
|
fi
|
|
|
|
# Private Subnets
|
|
echo ""
|
|
if [[ "$CREATE_NAT_GATEWAY" == "true" ]]; then
|
|
print_status "Private subnets will be used for ECS tasks and databases"
|
|
print_status "NAT Gateways will provide internet access for these subnets"
|
|
else
|
|
print_status "Private subnets will be used for ECS tasks and databases"
|
|
print_status "These subnets should have internet access via Transit Gateway"
|
|
fi
|
|
prompt_input "Private Subnet IDs (comma-separated, minimum 2)" "$PRIVATE_SUBNETS" "PRIVATE_SUBNETS"
|
|
|
|
if ! validate_subnets "$PRIVATE_SUBNETS" "Private"; then
|
|
exit 1
|
|
fi
|
|
|
|
# Secrets Manager VPC endpoint: only one per VPC can have private DNS; skip if another stack (e.g. dev) already created it
|
|
echo ""
|
|
print_prompt "Does another stack in this VPC already have a Secrets Manager VPC endpoint (e.g. dev)? (y/N): "
|
|
read -r sm_ep_choice
|
|
if [[ "$sm_ep_choice" =~ ^[Yy]$ ]]; then
|
|
CREATE_SECRETS_MANAGER_VPC_ENDPOINT="false"
|
|
print_success "Will not create Secrets Manager VPC endpoint (reusing existing in VPC)"
|
|
else
|
|
CREATE_SECRETS_MANAGER_VPC_ENDPOINT="true"
|
|
print_success "This stack will create the Secrets Manager VPC endpoint (with private DNS)"
|
|
fi
|
|
|
|
echo ""
|
|
print_status "Step 3: AWS Bedrock Credentials"
|
|
print_important "LibreChat needs AWS credentials to access Bedrock models."
|
|
print_important "These credentials will be securely stored in AWS Secrets Manager."
|
|
echo ""
|
|
|
|
# AWS Access Key ID
|
|
prompt_input "AWS Access Key ID for Bedrock access" "$AWS_ACCESS_KEY_ID" "AWS_ACCESS_KEY_ID"
|
|
|
|
# AWS Secret Access Key
|
|
prompt_input "AWS Secret Access Key for Bedrock access" "$AWS_SECRET_ACCESS_KEY" "AWS_SECRET_ACCESS_KEY"
|
|
|
|
# Validate Bedrock access
|
|
validate_bedrock_access "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" "$REGION"
|
|
|
|
echo ""
|
|
print_status "Step 4: SSO Configuration (Optional)"
|
|
print_important "Configure Single Sign-On with AWS Cognito for user authentication."
|
|
print_important ""
|
|
print_important "📋 BEFORE ENABLING SSO, ENSURE YOU HAVE:"
|
|
print_important "• Created a Cognito User Pool"
|
|
print_important "• Created an App Client in the User Pool"
|
|
print_important "• Configured the App Client with appropriate callback URLs"
|
|
print_important "• Set up attribute mappings (name, email) in the User Pool"
|
|
echo ""
|
|
|
|
# SSO Enable
|
|
print_prompt "Enable SSO authentication? (y/N): "
|
|
read -r sso_choice
|
|
if [[ "$sso_choice" =~ ^[Yy]$ ]]; then
|
|
ENABLE_SSO="true"
|
|
|
|
# Cognito User Pool ID
|
|
prompt_input "Cognito User Pool ID" "$COGNITO_USER_POOL_ID" "COGNITO_USER_POOL_ID"
|
|
|
|
# OpenID Client ID
|
|
prompt_input "OpenID Client ID (from Cognito App Client)" "$OPENID_CLIENT_ID" "OPENID_CLIENT_ID"
|
|
|
|
# OpenID Client Secret
|
|
prompt_input "OpenID Client Secret (from Cognito App Client)" "$OPENID_CLIENT_SECRET" "OPENID_CLIENT_SECRET"
|
|
|
|
# OpenID Scope
|
|
prompt_input "OpenID Scope" "$OPENID_SCOPE" "OPENID_SCOPE"
|
|
|
|
# OpenID Button Label
|
|
prompt_input "SSO Button Label" "$OPENID_BUTTON_LABEL" "OPENID_BUTTON_LABEL"
|
|
|
|
# OpenID Image URL
|
|
prompt_input "SSO Button Image URL" "$OPENID_IMAGE_URL" "OPENID_IMAGE_URL"
|
|
|
|
# OpenID Name Claim
|
|
prompt_input "Name Claim Attribute" "$OPENID_NAME_CLAIM" "OPENID_NAME_CLAIM"
|
|
|
|
# OpenID Email Claim
|
|
prompt_input "Email Claim Attribute" "$OPENID_EMAIL_CLAIM" "OPENID_EMAIL_CLAIM"
|
|
|
|
# Validate required SSO fields
|
|
if [[ -z "$COGNITO_USER_POOL_ID" || -z "$OPENID_CLIENT_ID" || -z "$OPENID_CLIENT_SECRET" ]]; then
|
|
print_error "All SSO fields are required when SSO is enabled"
|
|
exit 1
|
|
fi
|
|
|
|
print_success "SSO configuration completed"
|
|
else
|
|
ENABLE_SSO="false"
|
|
print_status "SSO authentication disabled - users will use email/password login"
|
|
fi
|
|
|
|
echo ""
|
|
print_status "Step 5: Application Configuration"
|
|
|
|
# Help and FAQ URL
|
|
prompt_input "Help and FAQ URL (use '/' to disable button)" "$HELP_AND_FAQ_URL" "HELP_AND_FAQ_URL"
|
|
|
|
echo ""
|
|
print_status "Step 5b: MCP Server Secrets"
|
|
print_status "Enter Bearer tokens for MCP servers that require auth. Press Enter to skip or use existing secret."
|
|
prompt_input "MCP Congress token" "$MCP_CONGRESS_TOKEN" "MCP_CONGRESS_TOKEN"
|
|
prompt_input "MCP Eastern Time token" "$MCP_EASTERN_TIME_TOKEN" "MCP_EASTERN_TIME_TOKEN"
|
|
|
|
echo ""
|
|
print_status "Step 6: SSL/Domain Configuration"
|
|
|
|
# Domain name
|
|
prompt_input "Domain Name (librechatchat.example.com)" "$DOMAIN_NAME" "DOMAIN_NAME"
|
|
|
|
# Certificate ARN
|
|
if [[ -n "$DOMAIN_NAME" ]]; then
|
|
prompt_input "ACM Certificate ARN (required for HTTPS)" "$CERTIFICATE_ARN" "CERTIFICATE_ARN"
|
|
fi
|
|
|
|
echo ""
|
|
print_status "Configuration Summary:"
|
|
echo " Environment: $ENVIRONMENT"
|
|
echo " Region: $REGION"
|
|
echo " Stack Name: $STACK_NAME"
|
|
echo " VPC ID: $VPC_ID"
|
|
echo " Public Subnets: $PUBLIC_SUBNETS"
|
|
echo " Private Subnets: $PRIVATE_SUBNETS"
|
|
echo " Create NAT Gateway: $CREATE_NAT_GATEWAY"
|
|
echo " Create Secrets Manager VPC Endpoint: $CREATE_SECRETS_MANAGER_VPC_ENDPOINT"
|
|
echo " AWS Access Key: ${AWS_ACCESS_KEY_ID:0:8}..."
|
|
echo " SSO Enabled: $ENABLE_SSO"
|
|
if [[ "$ENABLE_SSO" == "true" ]]; then
|
|
echo " Cognito User Pool: $COGNITO_USER_POOL_ID"
|
|
echo " OpenID Client ID: ${OPENID_CLIENT_ID:0:8}..."
|
|
echo " SSO Button Label: $OPENID_BUTTON_LABEL"
|
|
if [[ -n "$OPENID_IMAGE_URL" ]]; then
|
|
echo " SSO Button Image: $OPENID_IMAGE_URL"
|
|
fi
|
|
fi
|
|
if [[ -n "$HELP_AND_FAQ_URL" ]]; then
|
|
echo " Help & FAQ URL: $HELP_AND_FAQ_URL"
|
|
fi
|
|
if [[ -n "$DOMAIN_NAME" ]]; then
|
|
echo " Domain: $DOMAIN_NAME"
|
|
echo " Certificate: $CERTIFICATE_ARN"
|
|
fi
|
|
|
|
echo ""
|
|
print_important "🚀 DEPLOYMENT FEATURES:"
|
|
print_important "• ECS Fargate with auto-scaling (2-20 instances)"
|
|
print_important "• DocumentDB (MongoDB-compatible) with multi-AZ"
|
|
print_important "• ElastiCache Redis with failover"
|
|
print_important "• S3 for file storage with encryption"
|
|
print_important "• VPC endpoints for AWS services (reduced internet traffic)"
|
|
if [[ "$CREATE_NAT_GATEWAY" == "true" ]]; then
|
|
print_important "• NAT Gateways for reliable internet connectivity (~\$90/month)"
|
|
else
|
|
print_important "• Transit Gateway routing (no NAT Gateway costs)"
|
|
fi
|
|
|
|
echo ""
|
|
print_prompt "Save this configuration for future deployments? (y/N): "
|
|
read -r save_choice
|
|
if [[ "$save_choice" =~ ^[Yy]$ ]]; then
|
|
save_config
|
|
fi
|
|
|
|
echo ""
|
|
print_prompt "Proceed with deployment? (y/N): "
|
|
read -r deploy_choice
|
|
if [[ ! "$deploy_choice" =~ ^[Yy]$ ]]; then
|
|
print_status "Deployment cancelled"
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
# Parse command line arguments
|
|
LOAD_CONFIG=false
|
|
RESET_CONFIG=false
|
|
UPDATE_CONFIG_ONLY=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--load-config)
|
|
LOAD_CONFIG=true
|
|
shift
|
|
;;
|
|
--reset-config)
|
|
RESET_CONFIG=true
|
|
shift
|
|
;;
|
|
--update-config)
|
|
UPDATE_CONFIG_ONLY=true
|
|
LOAD_CONFIG=true # Auto-load config for update-only mode
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
print_error "Unknown option: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Handle reset config
|
|
if [[ "$RESET_CONFIG" == true ]]; then
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
rm "$CONFIG_FILE"
|
|
print_success "Configuration file deleted"
|
|
else
|
|
print_warning "No configuration file to delete"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Validate prerequisites
|
|
validate_aws
|
|
validate_sam
|
|
|
|
# Load existing config if requested
|
|
if [[ "$LOAD_CONFIG" == true ]]; then
|
|
if ! load_config; then
|
|
if [[ "$UPDATE_CONFIG_ONLY" == true ]]; then
|
|
print_error "Cannot update config without existing configuration. Run full deployment first."
|
|
exit 1
|
|
fi
|
|
print_status "Starting fresh configuration..."
|
|
fi
|
|
fi
|
|
|
|
# Handle config-only update
|
|
if [[ "$UPDATE_CONFIG_ONLY" == true ]]; then
|
|
print_status "Config-only update mode - updating configuration and restarting containers"
|
|
|
|
if [[ -z "$STACK_NAME" || -z "$REGION" ]]; then
|
|
print_error "Missing stack name or region in configuration"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify stack exists
|
|
if ! aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" &>/dev/null; then
|
|
print_error "Stack $STACK_NAME not found in region $REGION"
|
|
exit 1
|
|
fi
|
|
|
|
print_status "Updating configuration for stack: $STACK_NAME"
|
|
if update_config "$STACK_NAME" "$REGION"; then
|
|
print_success "Configuration updated successfully!"
|
|
print_status "Containers are restarting with the new configuration"
|
|
else
|
|
print_error "Configuration update failed"
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Run interactive configuration
|
|
interactive_config
|
|
|
|
# Ensure ECS service-linked role exists now that we have the region
|
|
if ! ensure_ecs_service_role "$REGION"; then
|
|
exit 1
|
|
fi
|
|
|
|
print_status "Starting LibreChat deployment..."
|
|
|
|
# Build the SAM application
|
|
print_status "Building SAM application..."
|
|
sam build
|
|
|
|
# Create or update MCP secrets in Secrets Manager when tokens are provided
|
|
MCP_CONGRESS_SECRET_ARN=""
|
|
MCP_EASTERN_TIME_SECRET_ARN=""
|
|
if [[ -n "$MCP_CONGRESS_TOKEN" ]]; then
|
|
print_status "Creating/updating MCP Congress secret in Secrets Manager..."
|
|
MCP_CONGRESS_SECRET_ARN=$(ensure_secret_string "${STACK_NAME}/mcp/congress" "$MCP_CONGRESS_TOKEN" "$REGION")
|
|
fi
|
|
if [[ -n "$MCP_EASTERN_TIME_TOKEN" ]]; then
|
|
print_status "Creating/updating MCP Eastern Time secret in Secrets Manager..."
|
|
MCP_EASTERN_TIME_SECRET_ARN=$(ensure_secret_string "${STACK_NAME}/mcp/eastern-time" "$MCP_EASTERN_TIME_TOKEN" "$REGION")
|
|
fi
|
|
|
|
|
|
# Create Bedrock credentials secret and OpenID client secret so we pass only ARNs to CloudFormation
|
|
BEDROCK_CREDENTIALS_SECRET_ARN=""
|
|
OPENID_CLIENT_SECRET_ARN=""
|
|
print_status "Creating/updating Bedrock credentials secret in Secrets Manager..."
|
|
BEDROCK_CREDENTIALS_SECRET_ARN=$(ensure_bedrock_credentials_secret "${STACK_NAME}/bedrock/credentials" "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" "$REGION")
|
|
if [[ "$ENABLE_SSO" == "true" && -n "$OPENID_CLIENT_SECRET" ]]; then
|
|
print_status "Creating/updating OpenID client secret in Secrets Manager..."
|
|
OPENID_CLIENT_SECRET_ARN=$(ensure_secret_string "${STACK_NAME}/openid/client-secret" "$OPENID_CLIENT_SECRET" "$REGION")
|
|
fi
|
|
|
|
# Prepare deployment parameters with proper quoting (only ARNs, no raw credentials)
|
|
DEPLOY_PARAMS=(
|
|
"Environment=$ENVIRONMENT"
|
|
"VpcId=$VPC_ID"
|
|
"PublicSubnetIds=$PUBLIC_SUBNETS"
|
|
"PrivateSubnetIds=$PRIVATE_SUBNETS"
|
|
"CreateNATGateway=$CREATE_NAT_GATEWAY"
|
|
"CreateSecretsManagerVPCEndpoint=$CREATE_SECRETS_MANAGER_VPC_ENDPOINT"
|
|
"BedrockCredentialsSecretArn=$BEDROCK_CREDENTIALS_SECRET_ARN"
|
|
"EnableSSO=$ENABLE_SSO"
|
|
)
|
|
if [[ -n "$LIBRECHAT_IMAGE" ]]; then
|
|
DEPLOY_PARAMS+=("LibreChatImage=$LIBRECHAT_IMAGE")
|
|
fi
|
|
|
|
if [[ "$ENABLE_SSO" == "true" ]]; then
|
|
DEPLOY_PARAMS+=(
|
|
"CognitoUserPoolId=$COGNITO_USER_POOL_ID"
|
|
"OpenIdClientId=$OPENID_CLIENT_ID"
|
|
"OpenIdScope=$OPENID_SCOPE"
|
|
"OpenIdButtonLabel=\"$OPENID_BUTTON_LABEL\""
|
|
"OpenIdNameClaim=$OPENID_NAME_CLAIM"
|
|
"OpenIdEmailClaim=$OPENID_EMAIL_CLAIM"
|
|
)
|
|
if [[ -n "$OPENID_CLIENT_SECRET_ARN" ]]; then
|
|
DEPLOY_PARAMS+=("OpenIdClientSecretArn=$OPENID_CLIENT_SECRET_ARN")
|
|
else
|
|
DEPLOY_PARAMS+=("OpenIdClientSecret=$OPENID_CLIENT_SECRET")
|
|
fi
|
|
|
|
# Add image URL if provided
|
|
if [[ -n "$OPENID_IMAGE_URL" ]]; then
|
|
DEPLOY_PARAMS+=("OpenIdImageUrl=\"$OPENID_IMAGE_URL\"")
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$HELP_AND_FAQ_URL" ]]; then
|
|
DEPLOY_PARAMS+=("HelpAndFaqUrl=$HELP_AND_FAQ_URL")
|
|
fi
|
|
|
|
if [[ -n "$DOMAIN_NAME" ]]; then
|
|
DEPLOY_PARAMS+=("DomainName=$DOMAIN_NAME")
|
|
fi
|
|
|
|
if [[ -n "$CERTIFICATE_ARN" ]]; then
|
|
DEPLOY_PARAMS+=("CertificateArn=$CERTIFICATE_ARN")
|
|
fi
|
|
|
|
if [[ -n "$MCP_CONGRESS_SECRET_ARN" ]]; then
|
|
DEPLOY_PARAMS+=("MCPCongressSecretArn=$MCP_CONGRESS_SECRET_ARN")
|
|
fi
|
|
if [[ -n "$MCP_EASTERN_TIME_SECRET_ARN" ]]; then
|
|
DEPLOY_PARAMS+=("MCPEasternTimeSecretArn=$MCP_EASTERN_TIME_SECRET_ARN")
|
|
fi
|
|
|
|
# When reusing existing Secrets Manager VPC endpoint, discover its SG so the stack can add this stack's ECS SG before ECS Service starts
|
|
if [[ "$CREATE_SECRETS_MANAGER_VPC_ENDPOINT" == "false" ]]; then
|
|
print_status "Discovering existing Secrets Manager VPC endpoint security group..."
|
|
EXISTING_SM_ENDPOINT_SG_ID=$(aws ec2 describe-vpc-endpoints --region "$REGION" \
|
|
--filters \
|
|
"Name=service-name,Values=com.amazonaws.${REGION}.secretsmanager" \
|
|
"Name=vpc-id,Values=$VPC_ID" \
|
|
"Name=vpc-endpoint-type,Values=Interface" \
|
|
--query 'VpcEndpoints[0].Groups[0].GroupId' --output text 2>/dev/null)
|
|
if [[ -z "$EXISTING_SM_ENDPOINT_SG_ID" || "$EXISTING_SM_ENDPOINT_SG_ID" == "None" ]]; then
|
|
print_error "CreateSecretsManagerVPCEndpoint is false but no Secrets Manager VPC endpoint found in VPC $VPC_ID. Create an endpoint in this VPC first, or set CreateSecretsManagerVPCEndpoint=true for the first stack."
|
|
exit 1
|
|
fi
|
|
DEPLOY_PARAMS+=("ExistingSecretsManagerEndpointSecurityGroupId=$EXISTING_SM_ENDPOINT_SG_ID")
|
|
fi
|
|
|
|
# Deploy the application
|
|
print_status "Deploying to AWS..."
|
|
sam deploy \
|
|
--stack-name "$STACK_NAME" \
|
|
--region "$REGION" \
|
|
--capabilities CAPABILITY_IAM \
|
|
--parameter-overrides "${DEPLOY_PARAMS[@]}" \
|
|
--confirm-changeset \
|
|
--resolve-s3
|
|
|
|
if [[ $? -eq 0 ]]; then
|
|
print_success "Deployment completed successfully!"
|
|
|
|
# Upload config to S3 and trigger EFS sync
|
|
print_status "Uploading configuration and syncing to EFS..."
|
|
if upload_config "$STACK_NAME" "$REGION"; then
|
|
print_success "Configuration deployed successfully"
|
|
else
|
|
print_warning "Configuration upload failed, but deployment succeeded"
|
|
fi
|
|
|
|
# Get the load balancer URL
|
|
LB_URL=$(aws cloudformation describe-stacks \
|
|
--stack-name "$STACK_NAME" \
|
|
--region "$REGION" \
|
|
--query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerURL`].OutputValue' \
|
|
--output text)
|
|
|
|
if [[ -n "$LB_URL" ]]; then
|
|
print_success "LibreChat is available at: $LB_URL"
|
|
print_status "Note: It may take a few minutes for the service to be fully available."
|
|
fi
|
|
|
|
# Show other important outputs
|
|
print_status "Getting deployment information..."
|
|
aws cloudformation describe-stacks \
|
|
--stack-name "$STACK_NAME" \
|
|
--region "$REGION" \
|
|
--query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' \
|
|
--output table
|
|
|
|
# Show SSO-specific information if enabled
|
|
if [[ "$ENABLE_SSO" == "true" ]]; then
|
|
echo ""
|
|
print_important "🔐 SSO CONFIGURATION SUMMARY:"
|
|
print_important "• SSO Authentication: ENABLED"
|
|
print_important "• Cognito User Pool: $COGNITO_USER_POOL_ID"
|
|
print_important "• OpenID Issuer: https://cognito-idp.$REGION.amazonaws.com/$COGNITO_USER_POOL_ID"
|
|
|
|
# Get callback URL from outputs
|
|
CALLBACK_URL=$(aws cloudformation describe-stacks \
|
|
--stack-name "$STACK_NAME" \
|
|
--region "$REGION" \
|
|
--query 'Stacks[0].Outputs[?OutputKey==`CallbackURL`].OutputValue' \
|
|
--output text 2>/dev/null)
|
|
|
|
if [[ -n "$CALLBACK_URL" ]]; then
|
|
print_important "• Callback URL: $CALLBACK_URL"
|
|
print_important ""
|
|
print_important "📝 COGNITO APP CLIENT CONFIGURATION:"
|
|
print_important "• Add this callback URL to your Cognito App Client settings"
|
|
print_important "• Ensure 'Authorization code grant' is enabled"
|
|
print_important "• Configure attribute mappings for 'name' and 'email'"
|
|
fi
|
|
else
|
|
echo ""
|
|
print_important "🔐 AUTHENTICATION: Email/Password login enabled"
|
|
fi
|
|
|
|
else
|
|
print_error "Deployment failed!"
|
|
exit 1
|
|
fi
|
|
|
|
print_status "Deployment complete!"
|
|
echo ""
|
|
print_important "🎉 DEPLOYMENT SUMMARY:"
|
|
print_important "• Environment: $ENVIRONMENT"
|
|
print_important "• Region: $REGION"
|
|
print_important "• Stack: $STACK_NAME"
|
|
if [[ -n "$LB_URL" ]]; then
|
|
print_important "• Application URL: $LB_URL"
|
|
fi
|
|
if [[ "$ENABLE_SSO" == "true" ]]; then
|
|
print_important "• Authentication: SSO (Cognito) + Email/Password disabled"
|
|
else
|
|
print_important "• Authentication: Email/Password login"
|
|
fi
|
|
if [[ -n "$HELP_AND_FAQ_URL" && "$HELP_AND_FAQ_URL" != "/" ]]; then
|
|
print_important "• Help & FAQ: $HELP_AND_FAQ_URL"
|
|
elif [[ "$HELP_AND_FAQ_URL" == "/" ]]; then
|
|
print_important "• Help & FAQ: Disabled"
|
|
fi
|
|
echo ""
|
|
print_important ""
|
|
print_important "🤖 PARTIAL LIST OF AVAILABLE BEDROCK MODELS:"
|
|
print_important "• Claude Opus 4 (us.anthropic.claude-opus-4-20250514-v1:0)"
|
|
print_important "• Claude Sonnet 4 (us.anthropic.claude-sonnet-4-20250514-v1:0)"
|
|
print_important "• Claude 3.7 Sonnet (us.anthropic.claude-3-7-sonnet-20250219-v1:0)"
|
|
print_important "• Claude 3.5 Haiku (us.anthropic.claude-3-5-haiku-20241022-v1:0)"
|
|
print_important "• Llama 3.3 70B (us.meta.llama3-3-70b-instruct-v1:0)"
|
|
print_important ""
|
|
print_important "💰 COST SAVINGS:"
|
|
if [[ "$CREATE_NAT_GATEWAY" == "true" ]]; then
|
|
print_important "• NAT Gateways provide reliable internet connectivity (~\$90/month)"
|
|
print_important "• High availability with automatic failover"
|
|
print_important "• Enterprise-grade performance and security"
|
|
else
|
|
print_important "• No NAT Gateway costs (~\$90/month saved)"
|
|
print_important "• Using existing Transit Gateway infrastructure"
|
|
print_important " SHOULD BE REVIEWED, might be too slow"
|
|
fi
|
|
print_important "• VPC endpoints reduce data transfer costs"
|
|
|
|
echo ""
|
|
print_status "To redeploy with the same configuration, run:"
|
|
print_status " $0 --load-config"
|
|
print_status ""
|
|
print_status "To update config file and restart containers only, run:"
|
|
print_status " $0 --update-config"
|
|
print_status ""
|
|
print_status "To start with fresh configuration, run:"
|
|
print_status " $0 --reset-config"
|
|
|
|
# Clean up temporary files
|
|
rm -f /tmp/lambda-response.json 2>/dev/null || true |