diff --git a/.gitignore b/.gitignore index c6bba59..556d697 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# AWS SAM +.aws-sam/ \ No newline at end of file diff --git a/ai-chat-api/serverless.yml b/ai-chat-api/serverless.yml deleted file mode 100644 index b6a78e9..0000000 --- a/ai-chat-api/serverless.yml +++ /dev/null @@ -1,158 +0,0 @@ -service: ai-chat-api - -stages: - default: - params: - modelId: meta.llama3-70b-instruct-v1:0 - customDomainNameChatApi: chat.${param:customDomainName} - dynamoDbUsageTableName: ${self:service}-usage-table-${sls:stage} - throttleMonthlyLimitUser: 20 - throttleMonthlyLimitGlobal: 4000 - -provider: - name: aws - runtime: nodejs20.x - iam: - role: - statements: - - Effect: Allow - Action: - # This permission is required for ConverseStreamCommand - - bedrock:InvokeModelWithResponseStream - Resource: - - arn:aws:bedrock:${aws:region}::foundation-model/${param:modelId} - - Effect: Allow - Action: - - dynamodb:Query - - dynamodb:Scan - - dynamodb:GetItem - - dynamodb:PutItem - - dynamodb:UpdateItem - - dynamodb:DeleteItem - Resource: - - Fn::GetAtt: - - DynamoDbUsageTable - - Arn -build: - esbuild: - # By default the @aws-sdk/* packages are marked as external because they are - # included in the Lambda runtime; however, the lambda runtime packages are - # behind the latest release. At the time of writing, the lambda runtime did - # not include the @aws-sdk/bedrock-runtime-client. By setting the external - # and exclude options to [], we instruct ESBuild to include these packages - # in the bundle. - exclude: - - "@aws-sdk/*" - - "!@aws-sdk/client-bedrock-runtime" - -functions: - api: - handler: handler.handler - timeout: 60 - url: - invokeMode: RESPONSE_STREAM - cors: true - environment: - MODEL_ID: ${param:modelId} - SHARED_TOKEN_SECRET: ${param:sharedTokenSecret} - USAGE_TABLE_NAME: ${param:dynamoDbUsageTableName} - THROTTLE_MONTHLY_LIMIT_USER: ${param:throttleMonthlyLimitUser} - THROTTLE_MONTHLY_LIMIT_GLOBAL: ${param:throttleMonthlyLimitGlobal} - -resources: - # This condition is used to determine whether the custom domain name is - # enabled. The CloudFront distribution and Route 53 record set group are - # only created if the custom domain name is enabled. Otherwise the default - # Lambda function URL is used. - Conditions: - CustomDomainNameEnabled: - Fn::Equals: - - ${param:customDomainNameEnabled} - - true - Resources: - DynamoDbUsageTable: - Type: AWS::DynamoDB::Table - Properties: - AttributeDefinitions: - - AttributeName: PK - AttributeType: S - - AttributeName: SK - AttributeType: S - KeySchema: - - AttributeName: PK - KeyType: HASH - - AttributeName: SK - KeyType: RANGE - BillingMode: PAY_PER_REQUEST - TableName: ${param:dynamoDbUsageTableName} - ApiCloudFrontDistribution: - Type: AWS::CloudFront::Distribution - Condition: CustomDomainNameEnabled - DeletionPolicy: Delete - Properties: - DistributionConfig: - Enabled: true - PriceClass: PriceClass_100 - HttpVersion: http2 - Comment: Distribution to support the custom domain name ${param:customDomainNameChatApi} for the AI Chat API Service - Origins: - - Id: ChatAPILambdaFunction - DomainName: - !Select [ - 2, - !Split ["/", !GetAtt ApiLambdaFunctionUrl.FunctionUrl], - ] - OriginPath: "" - CustomOriginConfig: - HTTPPort: 80 - HTTPSPort: 443 - OriginProtocolPolicy: https-only - OriginSSLProtocols: [TLSv1, TLSv1.1, TLSv1.2] - DefaultCacheBehavior: - TargetOriginId: ChatAPILambdaFunction - ViewerProtocolPolicy: redirect-to-https - Compress: true - AllowedMethods: - - HEAD - - DELETE - - POST - - GET - - OPTIONS - - PUT - - PATCH - ForwardedValues: - QueryString: true - Headers: - - Authorization - Cookies: - Forward: all - Aliases: - - ${param:customDomainNameChatApi} - ViewerCertificate: - SslSupportMethod: sni-only - MinimumProtocolVersion: TLSv1.2_2021 - AcmCertificateArn: ${param:customDomainCertificateARN} - ApiRecordSetGroup: - Type: AWS::Route53::RecordSetGroup - DeletionPolicy: Delete - Condition: CustomDomainNameEnabled - DependsOn: - - ApiCloudFrontDistribution - Properties: - HostedZoneName: ${param:customDomainName}. - RecordSets: - - Name: ${param:customDomainNameChatApi} - Type: A - AliasTarget: - HostedZoneId: Z2FDTNDATAQYW2 # Cloudfront default hosted zone ID - DNSName: { "Fn::GetAtt": [ApiCloudFrontDistribution, DomainName] } - Outputs: - Outputs: - # This exports the URL of the endpoint with the custom domain name, if it is - # available, otherwise it provides the default Lambda function URL. - ChatApiUrl: - Value: - Fn::If: - - CustomDomainNameEnabled - - !Sub "https://${param:customDomainNameChatApi}" - - !GetAtt ApiLambdaFunctionUrl.FunctionUrl diff --git a/auth/serverless.yml b/auth/serverless.yml deleted file mode 100644 index 7f23590..0000000 --- a/auth/serverless.yml +++ /dev/null @@ -1,106 +0,0 @@ -service: auth - -stages: - default: - params: - customDomainNameAuthAPI: api.${param:customDomainName} - dynamoDbUserTableName: user-table-${sls:stage} - -build: - esbuild: true - -provider: - name: aws - runtime: nodejs20.x - iam: - role: - statements: - - Effect: Allow - Action: - - dynamodb:Query - - dynamodb:Scan - - dynamodb:GetItem - - dynamodb:PutItem - - dynamodb:UpdateItem - - dynamodb:DeleteItem - Resource: - - Fn::GetAtt: - - DynamoDbUserTable - - Arn - - Fn::Sub: "${DynamoDbUserTable.Arn}/index/emailIndex" - - Effect: Allow - Action: - - events:PutEvents - Resource: - - ${param:eventBusArn} - environment: - USERS_TABLE_NAME: ${param:dynamoDbUserTableName} - EVENT_BUS_NAME: ${param:eventBusName} - PREFIX_PATH: /${self:service} - SHARED_TOKEN_SECRET: ${param:sharedTokenSecret} - -plugins: - - serverless-domain-manager - -custom: - customDomain: - domainName: api.${param:customDomainName} - certificateArn: ${param:customDomainCertificateARN} - stage: "" - endpointType: regional - apiType: http - autoDomain: true - enabled: ${param:customDomainNameEnabled} - basePath: ${self:service} - -functions: - api: - handler: src/handler.handler - events: - - httpApi: - path: /{proxy+} - method: any - -# Serverless Framework doesn't support creating DynamoDB tables natively. -# Instead, we can use the AWS CloudFormation syntax to create the table. -resources: - Conditions: - CustomDomainNameEnabled: - Fn::Equals: - - ${param:customDomainNameEnabled} - - true - Resources: - DynamoDbUserTable: - Type: AWS::DynamoDB::Table - Properties: - AttributeDefinitions: - - AttributeName: userId - AttributeType: S - - AttributeName: email - AttributeType: S - KeySchema: - - AttributeName: userId - KeyType: HASH - BillingMode: PAY_PER_REQUEST - TableName: ${param:dynamoDbUserTableName} - GlobalSecondaryIndexes: - - IndexName: emailIndex - KeySchema: - - AttributeName: email - KeyType: HASH - Projection: - ProjectionType: ALL - - Outputs: - UserTableArn: - Description: The ARN of the Users DynamoDB table - Value: - Fn::GetAtt: - - DynamoDbUserTable - - Arn - AuthApiUrl: - Value: - Fn::If: - - CustomDomainNameEnabled - - !Sub "https://${param:customDomainNameAuthAPI}/${self:service}" - - !GetAtt HttpApi.ApiEndpoint diff --git a/auth/src/handler.js b/auth/src/handler.js index a5e8d84..18ffcf3 100644 --- a/auth/src/handler.js +++ b/auth/src/handler.js @@ -256,4 +256,4 @@ app.use((req, res, next) => { }); }); -export const handler = serverless(app); +export const handler = serverless(app, { basePath: process.env.PREFIX_PATH || '/auth' }); diff --git a/business-api/handler.js b/business-api/handler.js index d74ea38..8bafee1 100644 --- a/business-api/handler.js +++ b/business-api/handler.js @@ -42,4 +42,4 @@ app.use((err, req, res, next) => { } }); -export const handler = serverless(app); +export const handler = serverless(app, { basePath: '/business' }); diff --git a/business-api/serverless.yml b/business-api/serverless.yml deleted file mode 100644 index d62021d..0000000 --- a/business-api/serverless.yml +++ /dev/null @@ -1,51 +0,0 @@ -service: business - -provider: - name: aws - runtime: nodejs20.x - environment: - SHARED_TOKEN_SECRET: ${param:sharedTokenSecret} - -plugins: - - serverless-domain-manager - -stages: - default: - params: - customDomainNameAuthAPI: api.${param:customDomainName} - -build: - esbuild: true - -custom: - customDomain: - domainName: ${param:customDomainNameAuthAPI} - certificateArn: ${param:customDomainCertificateARN} - stage: "" - endpointType: regional - apiType: http - autoDomain: true - enabled: ${param:customDomainNameEnabled} - basePath: ${self:service} - -functions: - api: - handler: handler.handler - events: - - httpApi: - path: /{proxy+} - method: any - -resources: - Conditions: - CustomDomainNameEnabled: - Fn::Equals: - - ${param:customDomainNameEnabled} - - true - Outputs: - BusinessApiUrl: - Value: - Fn::If: - - CustomDomainNameEnabled - - !Sub "https://${param:customDomainNameAuthAPI}/${self:service}" - - !GetAtt HttpApi.ApiEndpoint diff --git a/business-worker/serverless.yml b/business-worker/serverless.yml deleted file mode 100644 index c4b7bef..0000000 --- a/business-worker/serverless.yml +++ /dev/null @@ -1,21 +0,0 @@ -service: business-worker - -provider: - name: aws - runtime: nodejs20.x - -build: - esbuild: true - -# This creates a simple lambda function that listens to the event bus for the -# auth.register event. The event bus ARN is passed as a parameter to the -# service, which originates from the event-bus service as an output parameter. -functions: - worker: - handler: handler.handler - events: - - eventBridge: - eventBus: ${param:eventBusArn} - pattern: - source: - - auth.register diff --git a/event-bus/serverless.yml b/event-bus/serverless.yml deleted file mode 100644 index 7b08576..0000000 --- a/event-bus/serverless.yml +++ /dev/null @@ -1,30 +0,0 @@ -service: event-bus - -provider: - name: aws - runtime: nodejs20.x - -params: - default: - eventBusName: ${self:service}-${sls:stage} - -# The "resource" block is used to define CloudFormation resources. In this case -# it creates an EventBridge Event Bus using the defined in params.default.eventBusName. -resources: - Resources: - EventBus: - Type: "AWS::Events::EventBus" - Properties: - Name: ${param:eventBusName} - # The "output" block is used to define the output of the Event Bus ARN and - # name. This is used as input parameters by other services. - Outputs: - EventBusArn: - Description: The ARN of the EventBridge Event Bus - Value: - Fn::GetAtt: - - EventBus - - Arn - EventBusName: - Description: The name of the EventBridge Event Bus - Value: ${param:eventBusName} diff --git a/serverless-compose.yml b/serverless-compose.yml deleted file mode 100644 index d18e450..0000000 --- a/serverless-compose.yml +++ /dev/null @@ -1,48 +0,0 @@ -stages: - default: - params: - # This is used as the shared secret for generating JWT tokens. For non - # production stages, the default value is used. In production, the value - # should be set in SSM. - sharedTokenSecret: DEFAULT - # The customDomainName enables support for custom domain names. As long as - # this value is false, the subsequent parameters are ignored. However, - # they are referenced in the serverless.yml files of the services, - # therefore we define defaults such that the variables can be resolved. - customDomainNameEnabled: false - customDomainName: NA - customDomainCertificateARN: NA - prod: - params: - # In production the sharedTokenSecret should be set in SSM, but if it isn - # the fallback value is used. - sharedTokenSecret: ${ssm:/YOUR_AWS_SHARED_SECRET, "DEFAULT"} - # In production, the custom domain name is enabled, and therefore the - # customDomainCertificateARN value must be set. The README providers - # more details on setting up the Certificate. - customDomainNameEnabled: true - customDomainName: YOUR_DOMAIN_NAME - customDomainCertificateARN: YOUR_CERTIFICATE_ARN - -services: - eventBus: - path: ./event-bus - auth: - path: ./auth - params: - eventBusArn: ${eventBus.EventBusArn} - eventBusName: ${eventBus.EventBusName} - aiChatApi: - path: ./ai-chat-api - web: - path: ./website - params: - chatApiUrl: ${aiChatApi.ChatApiUrl} - authApiUrl: ${auth.AuthApiUrl} - businessApi: - path: ./business-api - businessWorker: - path: ./business-worker - params: - eventBusArn: ${eventBus.EventBusArn} - eventBusName: ${eventBus.EventBusName} diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..45b5b2a --- /dev/null +++ b/template.yaml @@ -0,0 +1,214 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: AWS AI Stack deployed via AWS SAM (No Serverless Framework Required) + +Globals: + Function: + Timeout: 30 + Runtime: nodejs20.x + Architectures: + - x86_64 + +Parameters: + SharedTokenSecret: + Type: String + Default: "DEFAULT" + ModelId: + Type: String + Default: "meta.llama3-70b-instruct-v1:0" + +Resources: + # ========================================== + # 1. Event Bus + # ========================================== + EventBus: + Type: AWS::Events::EventBus + Properties: + Name: !Sub "${AWS::StackName}-event-bus" + + # ========================================== + # 2. Auth Service + # ========================================== + DynamoDbUserTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: userId + AttributeType: S + - AttributeName: email + AttributeType: S + KeySchema: + - AttributeName: userId + KeyType: HASH + BillingMode: PAY_PER_REQUEST + GlobalSecondaryIndexes: + - IndexName: emailIndex + KeySchema: + - AttributeName: email + KeyType: HASH + Projection: + ProjectionType: ALL + + AuthApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: auth/src/handler.handler + Environment: + Variables: + USERS_TABLE_NAME: !Ref DynamoDbUserTable + EVENT_BUS_NAME: !Ref EventBus + PREFIX_PATH: /auth + SHARED_TOKEN_SECRET: !Ref SharedTokenSecret + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref DynamoDbUserTable + - EventBridgePutEventsPolicy: + EventBusName: !Ref EventBus + Events: + ApiEvent: + Type: HttpApi + Properties: + Path: /auth/{proxy+} + Method: ANY + Metadata: + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + EntryPoints: + - auth/src/handler.js + + # ========================================== + # 3. AI Chat API + # ========================================== + DynamoDbUsageTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: PK + AttributeType: S + - AttributeName: SK + AttributeType: S + KeySchema: + - AttributeName: PK + KeyType: HASH + - AttributeName: SK + KeyType: RANGE + BillingMode: PAY_PER_REQUEST + + AiChatApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: ai-chat-api/handler.handler + Timeout: 60 + Environment: + Variables: + MODEL_ID: !Ref ModelId + SHARED_TOKEN_SECRET: !Ref SharedTokenSecret + USAGE_TABLE_NAME: !Ref DynamoDbUsageTable + THROTTLE_MONTHLY_LIMIT_USER: "20" + THROTTLE_MONTHLY_LIMIT_GLOBAL: "4000" + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref DynamoDbUsageTable + - Statement: + - Effect: Allow + Action: bedrock:InvokeModelWithResponseStream + Resource: !Sub "arn:aws:bedrock:${AWS::Region}::foundation-model/${ModelId}" + FunctionUrlConfig: + AuthType: NONE + Cors: + AllowOrigins: + - "*" + AllowHeaders: + - "*" + AllowMethods: + - "*" + InvokeMode: RESPONSE_STREAM + Metadata: + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + EntryPoints: + - ai-chat-api/handler.js + + # ========================================== + # 4. Business API + # ========================================== + BusinessApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: business-api/handler.handler + Environment: + Variables: + SHARED_TOKEN_SECRET: !Ref SharedTokenSecret + Events: + ApiEvent: + Type: HttpApi + Properties: + Path: /business/{proxy+} + Method: ANY + Metadata: + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + EntryPoints: + - business-api/handler.js + + # ========================================== + # 5. Business Worker + # ========================================== + BusinessWorkerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: business-worker/handler.handler + Events: + AuthRegisterEvent: + Type: EventBridgeRule + Properties: + EventBusName: !Ref EventBus + Pattern: + source: + - auth.register + Metadata: + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + EntryPoints: + - business-worker/handler.js + + # ========================================== + # 6. Website (Frontend) + # ========================================== + WebsiteFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: website/ + Handler: index.handler + Events: + ApiEvent: + Type: HttpApi + Properties: + Path: /{proxy+} + Method: ANY + +Outputs: + WebsiteUrl: + Description: "API Gateway URL for Website (Frontend)" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/" + AuthApiUrl: + Description: "API Gateway URL for Auth API" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/auth" + BusinessApiUrl: + Description: "API Gateway URL for Business API" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/business" + ChatApiUrl: + Description: "Lambda Function URL for Chat API" + Value: !GetAtt AiChatApiFunctionUrl.FunctionUrl diff --git a/website/index.js b/website/index.js index 649e159..840efa2 100644 --- a/website/index.js +++ b/website/index.js @@ -1,7 +1,10 @@ import express from "express"; import path from "path"; import serverless from "serverless-http"; +import { fileURLToPath } from 'url'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const app = express(); /** diff --git a/website/serverless.yml b/website/serverless.yml deleted file mode 100644 index 7825964..0000000 --- a/website/serverless.yml +++ /dev/null @@ -1,40 +0,0 @@ -service: web - -provider: - name: aws - runtime: nodejs20.x - -plugins: - - serverless-domain-manager # Load the community plugin for custom domains installed with npm - - ./scripts # Load our custom scripts plugin - -build: - esbuild: true - -package: - patterns: - - "!app/**" - - app/build/** - -custom: - customDomain: - domainName: ${param:customDomainName} - certificateArn: ${param:customDomainCertificateARN} - stage: "" - endpointType: "regional" - apiType: http - autoDomain: true - enabled: ${param:customDomainNameEnabled} - - # This property is expected by our custom scripts plugin. - scripts: - hooks: - # This hook builds the React App. It sets the environment variables for - # the Chat and Auth APIs and then runs the build script. - "before:esbuild-package:package": cd website/app && VITE_CHAT_API_URL='${param:chatApiUrl}' VITE_AUTH_API_URL='${param:authApiUrl}' npm run build - -functions: - app: - handler: index.handler - events: - - httpApi: "*"