LLMアプリケーションの可観測性・分析プラットフォームである Langfuse を、Amazon ECS/Fargate と RDS PostgreSQL を用いてAWS上に安全にデプロイします。
AWS CLIを使用してCloudFormationスタックをデプロイする場合は、以下のコマンドを実行します。
aws cloudformation create-stack \ --stack-name langfuse-on-aws-stack \ --template-body file://langfuse-on-aws.yaml \ --capabilities CAPABILITY_IAM
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Langfuse on AWS with ECS Fargate and RDS PostgreSQL - Best Practices'
Parameters:
Environment:
Type: String
Default: prod
AllowedValues:
- dev
- prod
Description: Environment name
DBUsername:
Type: String
Default: langfuse
Description: Database master username
NoEcho: true
DBPassword:
Type: String
NoEcho: true
MinLength: 16
Description: Database master password (min 16 characters)
NextAuthSecret:
Type: String
NoEcho: true
MinLength: 32
Description: NextAuth secret for session encryption (min 32 characters)
Salt:
Type: String
NoEcho: true
MinLength: 32
Description: Salt for API key hashing (min 32 characters)
Resources:
# VPC
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-vpc'
# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-igw'
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# Public Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-public-1'
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-public-2'
# Private Subnets
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.11.0/24
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-private-1'
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.12.0/24
AvailabilityZone: !Select [1, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-private-2'
# Route Tables
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-public-rt'
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
# Security Groups
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ALB Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-alb-sg'
ECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ECS Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3000
ToPort: 3000
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-ecs-sg'
RDSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: RDS Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref ECSSecurityGroup
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-rds-sg'
# RDS Subnet Group
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-db-subnet-group'
# RDS PostgreSQL
PostgresDB:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
DBInstanceIdentifier: !Sub '${AWS::StackName}-postgres'
Engine: postgres
EngineVersion: '16.4'
DBInstanceClass: db.t4g.micro
AllocatedStorage: 20
StorageType: gp3
StorageEncrypted: true
MasterUsername: !Ref DBUsername
MasterUserPassword: !Ref DBPassword
DBSubnetGroupName: !Ref DBSubnetGroup
VPCSecurityGroups:
- !Ref RDSSecurityGroup
BackupRetentionPeriod: 7
PreferredBackupWindow: '03:00-04:00'
PreferredMaintenanceWindow: 'sun:04:00-sun:05:00'
EnableCloudwatchLogsExports:
- postgresql
DeletionProtection: true
PubliclyAccessible: false
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-postgres'
# Application Load Balancer
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub '${AWS::StackName}-alb'
Type: application
Scheme: internet-facing
IpAddressType: ipv4
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-alb'
ALBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${AWS::StackName}-tg'
Port: 3000
Protocol: HTTP
VpcId: !Ref VPC
TargetType: ip
HealthCheckEnabled: true
HealthCheckPath: /api/public/health
HealthCheckProtocol: HTTP
HealthCheckIntervalSeconds: 30
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Matcher:
HttpCode: '200'
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ALB
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ALBTargetGroup
# ECS Cluster
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub '${AWS::StackName}-cluster'
CapacityProviders:
- FARGATE
- FARGATE_SPOT
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE_SPOT
Weight: 1
Base: 0
# CloudWatch Logs
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/ecs/${AWS::StackName}'
RetentionInDays: 7
# ECS Task Execution Role
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
# ECS Task Definition
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub '${AWS::StackName}-task'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: '512'
Memory: '1024'
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
- Name: langfuse
Image: langfuse/langfuse:latest
PortMappings:
- ContainerPort: 3000
Protocol: tcp
Environment:
- Name: NODE_ENV
Value: production
- Name: DATABASE_URL
Value: !Sub 'postgresql://${DBUsername}:${DBPassword}@${PostgresDB.Endpoint.Address}:5432/postgres'
- Name: NEXTAUTH_URL
Value: !Sub 'http://${ALB.DNSName}'
- Name: NEXTAUTH_SECRET
Value: !Ref NextAuthSecret
- Name: SALT
Value: !Ref Salt
- Name: TELEMETRY_ENABLED
Value: 'false'
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: langfuse
HealthCheck:
Command:
- CMD-SHELL
- curl -f http://localhost:3000/api/public/health || exit 1
Interval: 30
Timeout: 5
Retries: 3
StartPeriod: 60
# ECS Service
ECSService:
Type: AWS::ECS::Service
DependsOn: ALBListener
Properties:
ServiceName: !Sub '${AWS::StackName}-service'
Cluster: !Ref ECSCluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 1
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ECSSecurityGroup
LoadBalancers:
- ContainerName: langfuse
ContainerPort: 3000
TargetGroupArn: !Ref ALBTargetGroup
HealthCheckGracePeriodSeconds: 120
# CloudWatch Alarms
HighCPUAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmDescription: Alert when CPU exceeds 80%
MetricName: CPUUtilization
Namespace: AWS/ECS
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 80
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: ClusterName
Value: !Ref ECSCluster
- Name: ServiceName
Value: !GetAtt ECSService.Name
HighMemoryAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmDescription: Alert when Memory exceeds 80%
MetricName: MemoryUtilization
Namespace: AWS/ECS
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 80
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: ClusterName
Value: !Ref ECSCluster
- Name: ServiceName
Value: !GetAtt ECSService.Name
Outputs:
LangfuseURL:
Description: Langfuse Application URL
Value: !Sub 'http://${ALB.DNSName}'
Export:
Name: !Sub '${AWS::StackName}-URL'
DatabaseEndpoint:
Description: RDS PostgreSQL Endpoint
Value: !GetAtt PostgresDB.Endpoint.Address
Export:
Name: !Sub '${AWS::StackName}-DBEndpoint'
ECSClusterName:
Description: ECS Cluster Name
Value: !Ref ECSCluster
Export:
Name: !Sub '${AWS::StackName}-ClusterName'
ALBDNSName:
Description: Application Load Balancer DNS Name
Value: !GetAtt ALB.DNSName
Export:
Name: !Sub '${AWS::StackName}-ALBDNSName'