├── .env.example ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── serverless-dify.ts ├── cdk.json ├── jest.config.js ├── lib ├── aurorapg-run-query │ ├── index.mjs │ ├── package-lock.json │ └── package.json ├── celery-broker-stack.ts ├── dify-stack.ts ├── file-store-stack.ts ├── ingress-stack.ts ├── metadata-store-stack.ts ├── network-stack.ts ├── redis-stack.ts ├── task-definitions │ ├── dify-api.ts │ ├── dify-plugin-daemon.ts │ ├── dify-sandbox.ts │ ├── dify-web.ts │ ├── dify-worker.ts │ ├── props.ts │ └── task-environments.ts └── vector-store-stack.ts ├── package-lock.json ├── package.json ├── resources ├── architecture.png └── serverless-dify.drawio ├── test └── serverless-dify.test.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | DIFY_API_IMAGE=langgenius/dify-api:latest 2 | DIFY_WEB_IMAGE=langgenius/dify-web:latest 3 | DIFY_SANDBOX_IMAGE=langgenius/dify-sandbox:latest 4 | DIFY_PLUGIN_DAEMON_IMAGE=langgenius/dify-plugin-daemon:latest-local 5 | 6 | SMTP_SERVER_HOST=email-smtp.us-east-1.amazonaws.com 7 | SMTP_SERVER_PORT=587 8 | SMTP_SERVER_USERNAME= 9 | SMTP_SERVER_PASSWORD= 10 | SMTP_SERVER_TLS_ENABLED=true 11 | SMTP_SERVER_SEND_FROM= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | *.js 4 | !jest.config.js 5 | *.d.ts 6 | node_modules 7 | 8 | !lib/enable-pgvector-function/node_modules 9 | 10 | # CDK asset staging directory 11 | .cdk.staging 12 | cdk.out 13 | cdk.context.json 14 | 15 | .env 16 | .env.* 17 | !.env.example -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sample-serverless-dify-stack 2 | 3 | This project provides an AWS CDK implementation for deploying [Dify.AI](https://dify.ai/) , an open-source LLMOps platform, in a serverless architecture on AWS. 4 | 5 | Dify.AI is a powerful tool for building AI-native applications. This serverless implementation leverages various AWS services to create a scalable, maintainable, and cost-effective deployment of Dify.AI. 6 | 7 | ![architecture](./resources/architecture.png) 8 | 9 | ### Key Features 10 | 11 | 1. **Simplified Container Deployment:** Leverages AWS Elastic Container Registry (ECR) as the container orchestration tool, significantly reducing the complexity and entry barrier of containerized deployment. 12 | 13 | 2. **Cost-Effective Scalability:** Implements AWS Serverless technology stack to ensure economic efficiency through elastic scaling, optimizing resource utilization based on actual demand. 14 | 15 | ### Requirements 16 | 17 | 1. Node.js (version 14.x or later) and TypeScript/JavaScript development environment 18 | 19 | 2. CDK bootstrapped in your target region, refer to [here](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) for detailed setup instructions. 20 | 21 | ### Deploy 22 | 23 | 1. Prepare env values, copy `.env.example` to `.env` and update the env values; 24 | 25 | 2. Deploy the cdk stack using following command: 26 | ``` 27 | cdk deploy --region --all --concurrency 5 --require-approval never 28 | ``` 29 | 3. After deploy, you can find the dify endpoint in output: 30 | ``` 31 | ✅ ServerlessDifyStack 32 | 33 | ✨ Deployment time: 435.37s 34 | 35 | Outputs: 36 | ServerlessDifyStack.DifyEndpoint = Server-Ingre-xxxxxxx-xxxxxxx.us-east-1.elb.amazonaws.com 37 | Stack ARN: 38 | arn:aws:cloudformation:us-east-1:xxxxxxxx:stack/ServerlessDifyStack/16d8e980-ed22-11ef-b28b-xxxxxxxxxx 39 | ``` 40 | -------------------------------------------------------------------------------- /bin/serverless-dify.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as dotenv from 'dotenv'; 4 | import 'source-map-support/register'; 5 | import { CeleryBrokerStack } from '../lib/celery-broker-stack'; 6 | import { DifyStack } from '../lib/dify-stack'; 7 | import { FileStoreStack } from '../lib/file-store-stack'; 8 | import { IngressStack } from '../lib/ingress-stack'; 9 | import { MetadataStoreStack } from '../lib/metadata-store-stack'; 10 | import { NetworkStack } from '../lib/network-stack'; 11 | import { RedisStack } from '../lib/redis-stack'; 12 | import { VectorStoreStack } from '../lib/vector-store-stack'; 13 | 14 | dotenv.config() 15 | 16 | // initialize cdk context 17 | const app = new cdk.App(); 18 | 19 | // create network environment 20 | const network = new NetworkStack(app, "ServerlessDifyNetworkStack", {}) 21 | 22 | const fileStore = new FileStoreStack(app, "ServerlessDifyFileStoreStack", {}) 23 | 24 | const ingress = new IngressStack(app, "ServerlessDifyIngressStack", { vpc: network.vpc, sg: network.ingressSg }) 25 | ingress.addDependency(network) 26 | 27 | const metadataStore = new MetadataStoreStack(app, "ServerlessDifyMetadataStoreStack", { vpc: network.vpc, sg: network.metadataStoreSg }) 28 | metadataStore.addDependency(network) 29 | 30 | const vectorStore = new VectorStoreStack(app, "ServerlessDifyVectorStoreStack", { vpc: network.vpc, sg: network.vectorStoreSg }) 31 | vectorStore.addDependency(network) 32 | 33 | const redis = new RedisStack(app, "ServerlessDifyRedisStack", { vpc: network.vpc, sg: network.redisSg }) 34 | redis.addDependency(network) 35 | 36 | const celeryBroker = new CeleryBrokerStack(app, "ServerlessDifyCeleryBrokerStack", { vpc: network.vpc, sg: network.celeryBrokerSg }) 37 | celeryBroker.addDependency(network) 38 | 39 | const dify = new DifyStack(app, "ServerlessDifyStack", { 40 | network: network.exportProps(), 41 | fileStore: fileStore.exportProps(), 42 | ingress: ingress.exportProps(), 43 | metadataStore: metadataStore.exportProps(), 44 | vectorStore: vectorStore.exportProps(), 45 | redis: redis.exportProps(), 46 | celeryBroker: celeryBroker.exportProps(), 47 | smtp: { 48 | host: process.env.SMTP_SERVER_HOST || 'email-smtp.us-east-1.amazonaws.com', 49 | port: process.env.SMTP_SERVER_PORT || '578', 50 | username: process.env.SMTP_SERVER_USERNAME || 'admin', 51 | password: process.env.SMTP_SERVER_PASSWORD || 'admin', 52 | tls: process.env.SMTP_SERVER_TLS_ENABLED == 'true' ? true : false, 53 | fromEmail: process.env.SMTP_SERVER_SEND_FROM || 'admin@example.com' 54 | }, 55 | difyImage: { 56 | api: process.env.DIFY_API_IMAGE || 'langgenius/dify-api:latest', 57 | web: process.env.DIFY_WEB_IMAGE || 'langgenius/dify-web:latest', 58 | sandbox: process.env.DIFY_SANDBOX_IMAGE || 'langgenius/dify-sandbox:latest', 59 | pluginDaemon: process.env.DIFY_PLUGIN_DAEMON_IMAGE || 'langgenius/dify-plugin-daemon:latest-local' 60 | } 61 | }) 62 | 63 | dify.addDependency(network) 64 | dify.addDependency(fileStore) 65 | dify.addDependency(ingress) 66 | dify.addDependency(metadataStore) 67 | dify.addDependency(vectorStore) 68 | dify.addDependency(redis) 69 | dify.addDependency(celeryBroker) -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/serverless-dify.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 38 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 39 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 40 | "@aws-cdk/aws-route53-patters:useCertificate": true, 41 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 42 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 43 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 44 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 45 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 46 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 47 | "@aws-cdk/aws-redshift:columnId": true, 48 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 49 | "@aws-cdk/aws-ec2:ipv4IgnoreEgressRule": true, 50 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 51 | "@aws-cdk/aws-kms:aliasNameRef": true, 52 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 54 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 55 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 56 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 57 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 58 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 59 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 60 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 61 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 62 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 63 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 64 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 65 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 66 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 67 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 68 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 69 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false 70 | } 71 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/aurorapg-run-query/index.mjs: -------------------------------------------------------------------------------- 1 | import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; 2 | import pkg from 'pg'; 3 | const { Client } = pkg; 4 | 5 | export const handler = async (event) => { 6 | console.log(event) 7 | 8 | const secretArn = process.env.SECRET_ARN; 9 | 10 | const secretsManager = new SecretsManagerClient({}); 11 | const secret = await secretsManager.send( 12 | new GetSecretValueCommand({ SecretId: secretArn }) 13 | ); 14 | 15 | const secretString = JSON.parse(secret.SecretString || '{}'); 16 | const c = new Client({ 17 | host: secretString.host, 18 | port: secretString.port.toString(), 19 | database: secretString.dbname, 20 | user: secretString.username, 21 | password: secretString.password 22 | }) 23 | 24 | try { 25 | await c.connect() 26 | const response = await c.query(event.query); 27 | console.log(response); 28 | return { Status: 'SUCCESS', Data: 'Extension created successfully' } 29 | } catch (error) { 30 | throw error 31 | } finally { 32 | await c.end() 33 | } 34 | } -------------------------------------------------------------------------------- /lib/aurorapg-run-query/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enable-pgvector-function", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "enable-pgvector-function", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "pg": "^8.13.3" 13 | } 14 | }, 15 | "node_modules/pg": { 16 | "version": "8.13.3", 17 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", 18 | "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", 19 | "license": "MIT", 20 | "dependencies": { 21 | "pg-connection-string": "^2.7.0", 22 | "pg-pool": "^3.7.1", 23 | "pg-protocol": "^1.7.1", 24 | "pg-types": "^2.1.0", 25 | "pgpass": "1.x" 26 | }, 27 | "engines": { 28 | "node": ">= 8.0.0" 29 | }, 30 | "optionalDependencies": { 31 | "pg-cloudflare": "^1.1.1" 32 | }, 33 | "peerDependencies": { 34 | "pg-native": ">=3.0.1" 35 | }, 36 | "peerDependenciesMeta": { 37 | "pg-native": { 38 | "optional": true 39 | } 40 | } 41 | }, 42 | "node_modules/pg-cloudflare": { 43 | "version": "1.1.1", 44 | "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", 45 | "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", 46 | "license": "MIT", 47 | "optional": true 48 | }, 49 | "node_modules/pg-connection-string": { 50 | "version": "2.7.0", 51 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", 52 | "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", 53 | "license": "MIT" 54 | }, 55 | "node_modules/pg-int8": { 56 | "version": "1.0.1", 57 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 58 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", 59 | "license": "ISC", 60 | "engines": { 61 | "node": ">=4.0.0" 62 | } 63 | }, 64 | "node_modules/pg-pool": { 65 | "version": "3.7.1", 66 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz", 67 | "integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==", 68 | "license": "MIT", 69 | "peerDependencies": { 70 | "pg": ">=8.0" 71 | } 72 | }, 73 | "node_modules/pg-protocol": { 74 | "version": "1.7.1", 75 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz", 76 | "integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==", 77 | "license": "MIT" 78 | }, 79 | "node_modules/pg-types": { 80 | "version": "2.2.0", 81 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 82 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 83 | "license": "MIT", 84 | "dependencies": { 85 | "pg-int8": "1.0.1", 86 | "postgres-array": "~2.0.0", 87 | "postgres-bytea": "~1.0.0", 88 | "postgres-date": "~1.0.4", 89 | "postgres-interval": "^1.1.0" 90 | }, 91 | "engines": { 92 | "node": ">=4" 93 | } 94 | }, 95 | "node_modules/pgpass": { 96 | "version": "1.0.5", 97 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", 98 | "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", 99 | "license": "MIT", 100 | "dependencies": { 101 | "split2": "^4.1.0" 102 | } 103 | }, 104 | "node_modules/postgres-array": { 105 | "version": "2.0.0", 106 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 107 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", 108 | "license": "MIT", 109 | "engines": { 110 | "node": ">=4" 111 | } 112 | }, 113 | "node_modules/postgres-bytea": { 114 | "version": "1.0.0", 115 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 116 | "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", 117 | "license": "MIT", 118 | "engines": { 119 | "node": ">=0.10.0" 120 | } 121 | }, 122 | "node_modules/postgres-date": { 123 | "version": "1.0.7", 124 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 125 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", 126 | "license": "MIT", 127 | "engines": { 128 | "node": ">=0.10.0" 129 | } 130 | }, 131 | "node_modules/postgres-interval": { 132 | "version": "1.2.0", 133 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 134 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 135 | "license": "MIT", 136 | "dependencies": { 137 | "xtend": "^4.0.0" 138 | }, 139 | "engines": { 140 | "node": ">=0.10.0" 141 | } 142 | }, 143 | "node_modules/split2": { 144 | "version": "4.2.0", 145 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", 146 | "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", 147 | "license": "ISC", 148 | "engines": { 149 | "node": ">= 10.x" 150 | } 151 | }, 152 | "node_modules/xtend": { 153 | "version": "4.0.2", 154 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 155 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 156 | "license": "MIT", 157 | "engines": { 158 | "node": ">=0.4" 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /lib/aurorapg-run-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enable-pgvector-function", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "description": "", 12 | "dependencies": { 13 | "pg": "^8.13.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/celery-broker-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { SecurityGroup, Vpc } from "aws-cdk-lib/aws-ec2"; 3 | import { CfnServerlessCache } from "aws-cdk-lib/aws-elasticache"; 4 | import { Construct } from "constructs"; 5 | import { DifyCeleryBrokerProps } from "./task-definitions/props"; 6 | 7 | export interface CeleryBrokerStackProps extends StackProps { 8 | 9 | vpc: Vpc 10 | 11 | sg: SecurityGroup 12 | } 13 | 14 | export class CeleryBrokerStack extends Stack { 15 | 16 | public readonly cluster: CfnServerlessCache 17 | 18 | constructor(scope: Construct, id: string, props: CeleryBrokerStackProps) { 19 | super(scope, id, props) 20 | 21 | this.cluster = new CfnServerlessCache(this, 'CeleryBroker', { 22 | engine: 'redis', 23 | serverlessCacheName: 'serverless-dify-celery-broker', 24 | description: 'serverless redis cluster using for celery broker', 25 | securityGroupIds: [props.sg.securityGroupId], 26 | subnetIds: props.vpc.privateSubnets.map(subnet => subnet.subnetId) 27 | }) 28 | 29 | } 30 | 31 | public exportProps(): DifyCeleryBrokerProps { 32 | return { hostname: this.cluster.attrEndpointAddress, port: this.cluster.attrEndpointPort } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /lib/dify-stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Duration, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; 2 | import { SecurityGroup, SubnetType } from "aws-cdk-lib/aws-ec2"; 3 | import { Cluster, FargateService } from "aws-cdk-lib/aws-ecs"; 4 | import { ApplicationListener, ApplicationProtocol, ListenerCondition } from "aws-cdk-lib/aws-elasticloadbalancingv2"; 5 | import { ManagedPolicy, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam"; 6 | import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs"; 7 | import { Secret } from "aws-cdk-lib/aws-secretsmanager"; 8 | import { PrivateDnsNamespace } from "aws-cdk-lib/aws-servicediscovery"; 9 | import { Construct } from "constructs"; 10 | import { DifyApiTaskDefinitionStack } from "./task-definitions/dify-api"; 11 | import { DifyPluginDaemonTaskDefinitionStack } from "./task-definitions/dify-plugin-daemon"; 12 | import { DifySandboxTaskDefinitionStack } from "./task-definitions/dify-sandbox"; 13 | import { DifyWebTaskDefinitionStack } from "./task-definitions/dify-web"; 14 | import { DifyWorkerTaskDefinitionStack } from "./task-definitions/dify-worker"; 15 | import { DifyCeleryBrokerProps, DifyFileStoreProps, DifyImage, DifyIngressProps, DifyMetadataStoreProps, DifyNetworkProps, DifyRedisProps, DifyTaskDefinitionStackProps, DifyVectorStorePgProps, SmtpServerProps } from "./task-definitions/props"; 16 | 17 | export interface DifyStackProps extends StackProps { 18 | 19 | readonly fileStore: DifyFileStoreProps 20 | 21 | readonly network: DifyNetworkProps 22 | 23 | readonly ingress: DifyIngressProps 24 | 25 | readonly celeryBroker: DifyCeleryBrokerProps 26 | 27 | readonly redis: DifyRedisProps 28 | 29 | readonly metadataStore: DifyMetadataStoreProps 30 | 31 | readonly vectorStore: DifyVectorStorePgProps 32 | 33 | readonly smtp: SmtpServerProps, 34 | 35 | readonly difyImage: DifyImage 36 | } 37 | 38 | export class DifyStack extends Stack { 39 | 40 | static readonly DIFY_API_SERVICE_DNS_NAME = "serverless-dify-api.local" 41 | 42 | static readonly DIFY_PLUGIN_DAEMON_SERVICE_DNS_NAME = "serverless-dify-plugin-daemon.local" 43 | 44 | static readonly DIFY_SANDBOX_SERVICE_DNS_NAME = "serverless-dify-sandbox.local" 45 | 46 | private readonly cluster: Cluster 47 | 48 | private readonly serviceNamespace: PrivateDnsNamespace 49 | 50 | private readonly taskSecurityGroup: SecurityGroup 51 | 52 | private readonly listener: ApplicationListener 53 | 54 | private props: DifyStackProps 55 | 56 | 57 | constructor(scope: Construct, id: string, props: DifyStackProps) { 58 | super(scope, id, props) 59 | 60 | this.props = props 61 | this.serviceNamespace = new PrivateDnsNamespace(this, 'ServleressDifyNamespace', { 62 | name: 'serverless-dify.local', 63 | vpc: props.network.vpc 64 | }) 65 | 66 | this.cluster = new Cluster(this, "ServerlessDifyEcsCluster", { vpc: props.network.vpc, enableFargateCapacityProviders: true }) 67 | this.cluster.node.addDependency(this.serviceNamespace) 68 | this.taskSecurityGroup = props.network.taskSecurityGroup 69 | 70 | this.listener = props.ingress.listener 71 | 72 | const difyTaskDefinitionStackProps: DifyTaskDefinitionStackProps = { 73 | network: props.network, fileStore: props.fileStore, 74 | celeryBroker: props.celeryBroker, redis: props.redis, 75 | metadataStore: props.metadataStore, vectorStore: props.vectorStore, 76 | apiSecretKey: new Secret(this, 'ServerlessDifyApiSecretKey', { generateSecretString: { passwordLength: 32 } }), 77 | pluginInnerApiKey: new Secret(this, 'ServerlessDifyPluginInnerApiKey', { generateSecretString: { passwordLength: 32 } }), 78 | pluginDaemonKey: new Secret(this, 'ServerlessDifyPluginDaemonKey', { generateSecretString: { passwordLength: 32 } }), 79 | sandboxCodeExecutionKey: new Secret(this, 'ServerlessDifySandboxCodeExecutionKey', { generateSecretString: { passwordLength: 32 } }), 80 | smtp: props.smtp, 81 | difyImage: props.difyImage, 82 | difyTaskRole: this.createTaskRole(), 83 | difyClusterLogGroup: new LogGroup(this, 'ServerlessDifyLogs', { retention: RetentionDays.ONE_WEEK, removalPolicy: RemovalPolicy.DESTROY }) 84 | } 85 | 86 | this.runSandboxService(difyTaskDefinitionStackProps) 87 | this.runApiService(difyTaskDefinitionStackProps) 88 | this.runPluginDaemonService(difyTaskDefinitionStackProps) 89 | this.runWorkService(difyTaskDefinitionStackProps) 90 | this.runWebService(difyTaskDefinitionStackProps) 91 | 92 | new CfnOutput(this, "DifyEndpoint", { value: props.ingress.lb.loadBalancerDnsName }) 93 | } 94 | 95 | createTaskRole() { 96 | const taskRole = new Role(this, "ServerlessDifyClusterSandboxTaskRole", { assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com") }); 97 | taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2ContainerRegistryPullOnly")); 98 | taskRole.addToPrincipalPolicy(new PolicyStatement({ 99 | actions: [ 100 | 'bedrock:InvokeModel', 101 | 'bedrock:InvokeModelWithResponseStream', 102 | 'bedrock:Rerank', 103 | 'bedrock:Retrieve', 104 | 'bedrock:RetrieveAndGenerate', 105 | 'logs:CreateLogGroup', 106 | 'logs:CreateLogStream', 107 | 'logs:PutLogEvents' 108 | ], 109 | resources: ['*'] 110 | })) 111 | 112 | this.props.fileStore.bucket.grantReadWrite(taskRole) 113 | return taskRole 114 | } 115 | 116 | runSandboxService(props: DifyTaskDefinitionStackProps) { 117 | const taskDefinition = new DifySandboxTaskDefinitionStack(this, 'DifySandboxTaskDefinitionStack', props) 118 | const service = new FargateService(this, 'ServerlessDifySandboxService', { 119 | cluster: this.cluster, 120 | taskDefinition: taskDefinition.definition, 121 | desiredCount: 1, 122 | serviceName: 'serverless-dify-sandbox', 123 | vpcSubnets: this.cluster.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }), 124 | securityGroups: [this.taskSecurityGroup], 125 | serviceConnectConfiguration: { 126 | namespace: this.serviceNamespace.namespaceName, 127 | services: [{ 128 | portMappingName: DifySandboxTaskDefinitionStack.SANDBOX_PORT_MAPPING_NAME, 129 | dnsName: DifyStack.DIFY_SANDBOX_SERVICE_DNS_NAME, 130 | port: DifySandboxTaskDefinitionStack.DIFY_SANDBOX_PORT, 131 | }] 132 | } 133 | }) 134 | 135 | return service 136 | } 137 | 138 | runApiService(props: DifyTaskDefinitionStackProps) { 139 | const taskDefinition = new DifyApiTaskDefinitionStack(this, 'DifyApiTaskDefinitionStack', props) 140 | const service = new FargateService(this, 'ServerlessDifyApiService', { 141 | cluster: this.cluster, 142 | taskDefinition: taskDefinition.definition, 143 | circuitBreaker: { rollback: true, enable: true }, 144 | desiredCount: 1, 145 | serviceName: 'serverless-dify-api', 146 | vpcSubnets: this.cluster.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }), 147 | securityGroups: [this.taskSecurityGroup], 148 | serviceConnectConfiguration: { 149 | namespace: this.serviceNamespace.namespaceName, 150 | services: [{ 151 | portMappingName: DifyApiTaskDefinitionStack.API_PORT_MAPPING_NAME, 152 | dnsName: DifyStack.DIFY_API_SERVICE_DNS_NAME, 153 | port: DifyApiTaskDefinitionStack.DIFY_API_PORT, 154 | }] 155 | } 156 | }) 157 | 158 | this.listener.addTargets('DifyApiTargets', { 159 | priority: 50000, 160 | targets: [service.loadBalancerTarget({ containerName: "main" })], 161 | conditions: [ListenerCondition.pathPatterns(["/console/api/*", "/api/*", "/v1/*", "/files/*"])], 162 | targetGroupName: "serverless-dify-api-tg", 163 | port: DifyApiTaskDefinitionStack.DIFY_API_PORT, 164 | protocol: ApplicationProtocol.HTTP, 165 | healthCheck: { 166 | path: DifyApiTaskDefinitionStack.HEALTHY_ENDPOINT, 167 | healthyHttpCodes: '200-400', 168 | interval: Duration.seconds(30), 169 | healthyThresholdCount: 3, 170 | unhealthyThresholdCount: 10 171 | } 172 | }) 173 | 174 | return service 175 | } 176 | 177 | runPluginDaemonService(props: DifyTaskDefinitionStackProps) { 178 | const taskDefinition = new DifyPluginDaemonTaskDefinitionStack(this, 'DifyPluginDaemonTaskDefinitionStack', props) 179 | const service = new FargateService(this, 'ServerlessDifyPluginDaemonService', { 180 | cluster: this.cluster, 181 | taskDefinition: taskDefinition.definition, 182 | circuitBreaker: { rollback: true, enable: true }, 183 | desiredCount: 1, 184 | serviceName: 'serverless-dify-plugin-daemon', 185 | vpcSubnets: this.cluster.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }), 186 | securityGroups: [this.taskSecurityGroup], 187 | serviceConnectConfiguration: { 188 | namespace: this.serviceNamespace.namespaceName, 189 | services: [ 190 | { 191 | portMappingName: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_PORT_NAME, 192 | dnsName: DifyStack.DIFY_PLUGIN_DAEMON_SERVICE_DNS_NAME, 193 | port: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_PORT, 194 | }, 195 | { 196 | portMappingName: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT_NAME, 197 | dnsName: DifyStack.DIFY_PLUGIN_DAEMON_SERVICE_DNS_NAME, 198 | port: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT, 199 | } 200 | ] 201 | } 202 | }) 203 | 204 | return service 205 | } 206 | 207 | runWorkService(props: DifyTaskDefinitionStackProps) { 208 | const taskDefinition = new DifyWorkerTaskDefinitionStack(this, 'DifyWorkerTaskDefinitionStack', props) 209 | const service = new FargateService(this, 'ServerlessDifyWorkerService', { 210 | cluster: this.cluster, 211 | taskDefinition: taskDefinition.definition, 212 | desiredCount: 1, 213 | serviceName: 'serverless-dify-worker', 214 | vpcSubnets: this.cluster.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }), 215 | securityGroups: [this.taskSecurityGroup], 216 | }) 217 | 218 | return service 219 | } 220 | 221 | runWebService(props: DifyTaskDefinitionStackProps) { 222 | const taskDefinition = new DifyWebTaskDefinitionStack(this, 'DifyWebServiceTaskDefinitionStack', props) 223 | const service = new FargateService(this, 'ServerlessDifyWebService', { 224 | cluster: this.cluster, 225 | taskDefinition: taskDefinition.definition, 226 | desiredCount: 1, 227 | serviceName: 'serverless-dify-web', 228 | vpcSubnets: this.cluster.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }), 229 | securityGroups: [this.taskSecurityGroup], 230 | }) 231 | 232 | this.listener.addTargets('DifyWebTargets', { 233 | targets: [service.loadBalancerTarget({ containerName: "main" })], 234 | targetGroupName: "serverless-dify-web-tg", 235 | port: DifyWebTaskDefinitionStack.DIFY_WEB_PORT, 236 | protocol: ApplicationProtocol.HTTP, 237 | healthCheck: { 238 | path: DifyWebTaskDefinitionStack.HEALTHY_ENDPOINT, 239 | healthyHttpCodes: '200', 240 | interval: Duration.seconds(30), 241 | timeout: Duration.seconds(5) 242 | } 243 | }) 244 | 245 | return service 246 | } 247 | } -------------------------------------------------------------------------------- /lib/file-store-stack.ts: -------------------------------------------------------------------------------- 1 | import { Duration, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; 2 | import { Bucket } from "aws-cdk-lib/aws-s3"; 3 | import { Construct } from "constructs"; 4 | import { DifyFileStoreProps } from "./task-definitions/props"; 5 | 6 | export class FileStoreStack extends Stack { 7 | 8 | public readonly bucket: Bucket; 9 | 10 | constructor(scope: Construct, id: string, props: StackProps) { 11 | super(scope, id, props) 12 | 13 | 14 | this.bucket = new Bucket(this, 'ServerlessDifyObjectFileStore', { 15 | removalPolicy: RemovalPolicy.DESTROY, 16 | versioned: true, 17 | }); 18 | 19 | this.bucket.addLifecycleRule({ 20 | abortIncompleteMultipartUploadAfter: Duration.days(1), 21 | noncurrentVersionExpiration: Duration.days(7), 22 | noncurrentVersionsToRetain: 2, 23 | }) 24 | } 25 | 26 | public exportProps(): DifyFileStoreProps { 27 | return { bucket: this.bucket } 28 | } 29 | } -------------------------------------------------------------------------------- /lib/ingress-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2"; 3 | import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol } from "aws-cdk-lib/aws-elasticloadbalancingv2"; 4 | import { Construct } from "constructs"; 5 | import { DifyIngressProps } from "./task-definitions/props"; 6 | 7 | export interface IngressStackProps extends StackProps { 8 | 9 | vpc: Vpc 10 | 11 | sg: SecurityGroup 12 | 13 | } 14 | 15 | export class IngressStack extends Stack { 16 | 17 | public readonly alb: ApplicationLoadBalancer 18 | 19 | public readonly listener: ApplicationListener 20 | 21 | constructor(scope: Construct, id: string, props: IngressStackProps) { 22 | super(scope, id, props) 23 | 24 | this.alb = new ApplicationLoadBalancer(this, "IngressStack", { 25 | vpc: props.vpc, 26 | securityGroup: props.sg, 27 | internetFacing: true, 28 | vpcSubnets: { subnetType: SubnetType.PUBLIC } 29 | }) 30 | 31 | this.listener = this.alb.addListener("IngressListener80", { 32 | port: 80, 33 | protocol: ApplicationProtocol.HTTP, 34 | }) 35 | } 36 | 37 | public exportProps(): DifyIngressProps { 38 | 39 | return { 40 | lb: this.alb, 41 | listener: this.listener, 42 | } 43 | 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /lib/metadata-store-stack.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; 2 | import { SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2"; 3 | import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam"; 4 | import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; 5 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 6 | import { RetentionDays } from "aws-cdk-lib/aws-logs"; 7 | import { AuroraPostgresEngineVersion, ClusterInstance, Credentials, DatabaseCluster, DatabaseClusterEngine, SubnetGroup } from "aws-cdk-lib/aws-rds"; 8 | import { Secret } from "aws-cdk-lib/aws-secretsmanager"; 9 | import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources"; 10 | import { InvocationType } from "aws-cdk-lib/triggers"; 11 | import { Construct } from "constructs"; 12 | import { DifyMetadataStoreProps } from "./task-definitions/props"; 13 | import path = require("path"); 14 | 15 | 16 | export interface MetadataStoreStackProps extends StackProps { 17 | 18 | vpc: Vpc 19 | 20 | sg: SecurityGroup 21 | 22 | } 23 | 24 | 25 | export class MetadataStoreStack extends Stack { 26 | 27 | public readonly cluster: DatabaseCluster 28 | 29 | public readonly secret: Secret 30 | 31 | static readonly PORT: number = 5432 32 | 33 | static readonly DEFAULT_DATABASE: string = "dify" 34 | 35 | static readonly DEFAULT_PLUGIN_DATABASE: string = "dify_plugin" 36 | 37 | private readonly sqlExecFunction: Function 38 | 39 | constructor(scope: Construct, id: string, props: MetadataStoreStackProps) { 40 | super(scope, id, props) 41 | 42 | const subnetGroup = new SubnetGroup(this, "MetadataStoreSubnetGroup", { 43 | description: 'Used for serverless-dify metadata store', 44 | vpc: props.vpc, 45 | removalPolicy: RemovalPolicy.DESTROY, 46 | vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS }, 47 | }); 48 | 49 | this.secret = new Secret(this, "MetadataStoreDatabaseSecret", { 50 | description: "Used for serverless-dify metadata store", 51 | generateSecretString: { 52 | secretStringTemplate: JSON.stringify({ username: "postgre" }), 53 | generateStringKey: "password", 54 | excludePunctuation: true 55 | } 56 | }) 57 | 58 | this.cluster = new DatabaseCluster(this, 'MetadataStoreDatabaseCluster', { 59 | vpc: props.vpc, 60 | securityGroups: [props.sg], 61 | port: MetadataStoreStack.PORT, 62 | subnetGroup: subnetGroup, 63 | clusterIdentifier: 'serverless-dify-metadata-store', 64 | engine: DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.VER_16_6 }), 65 | writer: ClusterInstance.serverlessV2('MetadataStoreServerlessWriteInstance'), 66 | cloudwatchLogsRetention: RetentionDays.ONE_WEEK, 67 | serverlessV2MinCapacity: 0, 68 | serverlessV2MaxCapacity: 2, 69 | iamAuthentication: true, 70 | // enableDataApi: true, 71 | credentials: Credentials.fromSecret(this.secret), 72 | defaultDatabaseName: MetadataStoreStack.DEFAULT_DATABASE, 73 | }) 74 | 75 | this.sqlExecFunction = new NodejsFunction(this, 'MetadataStoreSqlExecFunction', { 76 | runtime: Runtime.NODEJS_LATEST, 77 | handler: 'index.handler', 78 | memorySize: 256, 79 | environment: { SECRET_ARN: this.secret.secretArn }, 80 | code: Code.fromAsset(path.join(__dirname, './aurorapg-run-query/')), 81 | securityGroups: [props.sg], 82 | vpc: props.vpc, 83 | vpcSubnets: props.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }), 84 | logRetention: RetentionDays.ONE_DAY 85 | }) 86 | 87 | this.cluster.applyRemovalPolicy(RemovalPolicy.DESTROY) 88 | this.cluster.secret?.grantRead(this.sqlExecFunction) 89 | 90 | const query = this.createDifyPluginDatabase() 91 | this.cluster.secret?.grantRead(query) 92 | query.node.addDependency(this.cluster) 93 | } 94 | 95 | private createDifyPluginDatabase() { 96 | const query = new AwsCustomResource(this, 'CreateDifyPluginDatabase', { 97 | onUpdate: { 98 | service: 'Lambda', 99 | action: 'invoke', 100 | parameters: { 101 | FunctionName: this.sqlExecFunction.functionArn, 102 | InvocationType: InvocationType.REQUEST_RESPONSE, 103 | Payload: JSON.stringify({ 104 | query: `CREATE DATABASE IF NOT EXISTS ${MetadataStoreStack.DEFAULT_PLUGIN_DATABASE};` 105 | }), 106 | }, 107 | physicalResourceId: PhysicalResourceId.of('CreateDifyPluginDatabase') 108 | }, 109 | policy: AwsCustomResourcePolicy.fromStatements([ 110 | new PolicyStatement({ 111 | effect: Effect.ALLOW, 112 | actions: ['lambda:InvokeFunction'], 113 | resources: [this.sqlExecFunction.functionArn] 114 | }) 115 | ]) 116 | }) 117 | 118 | return query 119 | } 120 | 121 | 122 | public exportProps(): DifyMetadataStoreProps { 123 | return { 124 | hostname: this.cluster.clusterEndpoint.hostname, 125 | port: MetadataStoreStack.PORT, 126 | defaultDatabase: MetadataStoreStack.DEFAULT_DATABASE, 127 | secret: this.secret 128 | } 129 | } 130 | 131 | } -------------------------------------------------------------------------------- /lib/network-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { GatewayVpcEndpointAwsService, IpAddresses, Port, SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2"; 3 | import { Construct } from "constructs"; 4 | import { MetadataStoreStack } from "./metadata-store-stack"; 5 | import { DifyNetworkProps } from "./task-definitions/props"; 6 | import { VectorStoreStack } from "./vector-store-stack"; 7 | 8 | export class NetworkStack extends Stack { 9 | 10 | public readonly vpc: Vpc; 11 | 12 | public readonly ingressSg: SecurityGroup; 13 | 14 | public readonly difyServiceSg: SecurityGroup; 15 | 16 | public readonly metadataStoreSg: SecurityGroup; 17 | 18 | public readonly vectorStoreSg: SecurityGroup; 19 | 20 | public readonly redisSg: SecurityGroup; 21 | 22 | public readonly celeryBrokerSg: SecurityGroup; 23 | 24 | constructor(scope: Construct, id: string, props: StackProps) { 25 | super(scope, id, props) 26 | 27 | this.vpc = new Vpc(this, 'ServerlessDifyNetworkVpcStack', { 28 | enableDnsHostnames: true, 29 | enableDnsSupport: true, 30 | ipAddresses: IpAddresses.cidr("10.0.0.0/16"), 31 | 32 | natGateways: 1, 33 | natGatewaySubnets: { subnetType: SubnetType.PUBLIC }, 34 | 35 | gatewayEndpoints: { S3: { service: GatewayVpcEndpointAwsService.S3 } } 36 | }); 37 | 38 | this.ingressSg = new SecurityGroup(this, 'IngressSg', { vpc: this.vpc, allowAllOutbound: true }) 39 | this.ingressSg.connections.allowFromAnyIpv4(Port.HTTP) 40 | this.ingressSg.connections.allowFromAnyIpv4(Port.HTTPS) 41 | this.ingressSg.connections.allowFromAnyIpv4(Port.allIcmp()) 42 | 43 | this.difyServiceSg = new SecurityGroup(this, 'DifyServiceSg', { vpc: this.vpc, allowAllOutbound: true }) 44 | this.difyServiceSg.connections.allowInternally(Port.allTraffic()) 45 | this.difyServiceSg.connections.allowFrom(this.ingressSg, Port.allTcp()) 46 | 47 | this.metadataStoreSg = new SecurityGroup(this, 'MetadataStoreSg', { vpc: this.vpc, allowAllOutbound: true }); 48 | this.metadataStoreSg.connections.allowInternally(Port.allTraffic()) 49 | this.metadataStoreSg.connections.allowFrom(this.difyServiceSg, Port.tcp(MetadataStoreStack.PORT)) 50 | 51 | this.vectorStoreSg = new SecurityGroup(this, 'VectorStoreSg', { vpc: this.vpc, allowAllOutbound: true }); 52 | this.vectorStoreSg.connections.allowInternally(Port.allTraffic()) 53 | this.vectorStoreSg.connections.allowFrom(this.difyServiceSg, Port.tcp(VectorStoreStack.PORT)) 54 | 55 | this.redisSg = new SecurityGroup(this, 'RedisSg', { vpc: this.vpc, allowAllOutbound: true }); 56 | this.redisSg.connections.allowInternally(Port.allTraffic()) 57 | this.redisSg.connections.allowFrom(this.difyServiceSg, Port.tcp(6379)) 58 | 59 | this.celeryBrokerSg = new SecurityGroup(this, 'CeleryBrokerSg', { vpc: this.vpc, allowAllOutbound: true }); 60 | this.celeryBrokerSg.connections.allowInternally(Port.allTraffic()) 61 | this.celeryBrokerSg.connections.allowFrom(this.difyServiceSg, Port.tcp(6379)) 62 | } 63 | 64 | public exportProps(): DifyNetworkProps { 65 | return { vpc: this.vpc, taskSecurityGroup: this.difyServiceSg } 66 | } 67 | } 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /lib/redis-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { SecurityGroup, Vpc } from "aws-cdk-lib/aws-ec2"; 3 | import { CfnServerlessCache } from "aws-cdk-lib/aws-elasticache"; 4 | import { Construct } from "constructs"; 5 | import { DifyRedisProps } from "./task-definitions/props"; 6 | 7 | export interface RedisStackProps extends StackProps { 8 | 9 | vpc: Vpc, 10 | 11 | sg: SecurityGroup 12 | 13 | } 14 | 15 | export class RedisStack extends Stack { 16 | 17 | public readonly cluster: CfnServerlessCache 18 | 19 | constructor(scope: Construct, id: string, props: RedisStackProps) { 20 | super(scope, id, props) 21 | 22 | this.cluster = new CfnServerlessCache(this, 'RedisCluster', { 23 | engine: 'redis', 24 | serverlessCacheName: 'serverless-dify-redis', 25 | description: 'serverless redis cluster using for dify redis', 26 | securityGroupIds: [props.sg.securityGroupId], 27 | subnetIds: props.vpc.privateSubnets.map(subnet => subnet.subnetId) 28 | }) 29 | } 30 | 31 | public exportProps(): DifyRedisProps { 32 | return { 33 | hostname: this.cluster.attrEndpointAddress, 34 | port: this.cluster.attrEndpointPort 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /lib/task-definitions/dify-api.ts: -------------------------------------------------------------------------------- 1 | import { Duration, NestedStack } from "aws-cdk-lib"; 2 | import { AppProtocol, AwsLogDriverMode, Compatibility, ContainerImage, CpuArchitecture, LogDriver, NetworkMode, OperatingSystemFamily, Protocol, TaskDefinition } from "aws-cdk-lib/aws-ecs"; 3 | import { Construct } from "constructs"; 4 | import { DifyTaskDefinitionStackProps } from "./props"; 5 | import { TaskEnvironments } from "./task-environments"; 6 | 7 | export class DifyApiTaskDefinitionStack extends NestedStack { 8 | 9 | static readonly DIFY_API_PORT = 5001 10 | 11 | static readonly HEALTHY_ENDPOINT = "/health" 12 | 13 | static readonly API_PORT_MAPPING_NAME = "serverless-dify-api-5001-tcp" 14 | 15 | public readonly definition: TaskDefinition 16 | 17 | constructor(scope: Construct, id: string, props: DifyTaskDefinitionStackProps) { 18 | super(scope, id, props); 19 | 20 | this.definition = new TaskDefinition(this, 'DifyApiTaskDefinitionStack', { 21 | taskRole: props.difyTaskRole, 22 | executionRole: props.difyTaskRole, 23 | compatibility: Compatibility.EC2_AND_FARGATE, 24 | networkMode: NetworkMode.AWS_VPC, 25 | runtimePlatform: { 26 | operatingSystemFamily: OperatingSystemFamily.LINUX, 27 | cpuArchitecture: CpuArchitecture.X86_64 28 | }, 29 | cpu: '1024', 30 | memoryMiB: '2048', 31 | }) 32 | 33 | this.definition.addContainer('api', { 34 | containerName: "main", 35 | essential: true, 36 | image: ContainerImage.fromRegistry(props.difyImage.api), 37 | portMappings: [ 38 | { 39 | containerPort: DifyApiTaskDefinitionStack.DIFY_API_PORT, 40 | hostPort: DifyApiTaskDefinitionStack.DIFY_API_PORT, 41 | name: DifyApiTaskDefinitionStack.API_PORT_MAPPING_NAME, 42 | appProtocol: AppProtocol.http, protocol: Protocol.TCP 43 | } 44 | ], 45 | logging: LogDriver.awsLogs({ 46 | streamPrefix: 'api', 47 | mode: AwsLogDriverMode.NON_BLOCKING, 48 | logGroup: props.difyClusterLogGroup, 49 | }), 50 | healthCheck: { 51 | command: ['CMD-SHELL', 'curl -f http://localhost:5001/health || exit 1'], 52 | interval: Duration.seconds(15), 53 | startPeriod: Duration.seconds(90), 54 | retries: 10, 55 | timeout: Duration.seconds(5) 56 | }, 57 | 58 | environment: TaskEnvironments.getApiEnvironment(props, this.region), 59 | secrets: TaskEnvironments.getApiSecretEnvironment(props), 60 | }) 61 | } 62 | } -------------------------------------------------------------------------------- /lib/task-definitions/dify-plugin-daemon.ts: -------------------------------------------------------------------------------- 1 | import { NestedStack } from "aws-cdk-lib"; 2 | import { AppProtocol, AwsLogDriverMode, Compatibility, ContainerImage, CpuArchitecture, LogDriver, NetworkMode, OperatingSystemFamily, Protocol, TaskDefinition } from "aws-cdk-lib/aws-ecs"; 3 | import { Construct } from "constructs"; 4 | import { DifyTaskDefinitionStackProps } from "./props"; 5 | import { TaskEnvironments } from "./task-environments"; 6 | 7 | export class DifyPluginDaemonTaskDefinitionStack extends NestedStack { 8 | 9 | static readonly DIFY_PLUGIN_DAEMON_PORT = 5002 10 | static readonly DIFY_PLUGIN_DAEMON_PORT_NAME = 'serverless-dify-plugin-daemon-5002-tcp' 11 | 12 | static readonly DIFY_PLUGIN_DAEMON_DEBUG_PORT = 5003 13 | static readonly DIFY_PLUGIN_DAEMON_DEBUG_PORT_NAME = 'serverless-dify-plugin-daemon-5003-tcp' 14 | 15 | 16 | public readonly definition: TaskDefinition 17 | 18 | constructor(scope: Construct, id: string, props: DifyTaskDefinitionStackProps) { 19 | super(scope, id, props); 20 | 21 | this.definition = new TaskDefinition(this, 'ServerlessDifyPluginDaemonTaskDefinition', { 22 | taskRole: props.difyTaskRole, 23 | executionRole: props.difyTaskRole, 24 | compatibility: Compatibility.EC2_AND_FARGATE, 25 | networkMode: NetworkMode.AWS_VPC, 26 | runtimePlatform: { 27 | operatingSystemFamily: OperatingSystemFamily.LINUX, 28 | cpuArchitecture: CpuArchitecture.X86_64 29 | }, 30 | cpu: '256', 31 | memoryMiB: '512', 32 | }) 33 | 34 | this.definition.addContainer('plugin-daemon', { 35 | containerName: 'main', 36 | essential: true, 37 | image: ContainerImage.fromRegistry(props.difyImage.pluginDaemon), 38 | portMappings: [ 39 | { 40 | containerPort: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_PORT, 41 | hostPort: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_PORT, 42 | name: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_PORT_NAME, 43 | appProtocol: AppProtocol.http, protocol: Protocol.TCP 44 | }, 45 | { 46 | containerPort: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT, 47 | hostPort: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT, 48 | name: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT_NAME, 49 | appProtocol: AppProtocol.http, protocol: Protocol.TCP 50 | } 51 | ], 52 | logging: LogDriver.awsLogs({ 53 | streamPrefix: 'serverless-dify-plugin-daemon', 54 | mode: AwsLogDriverMode.NON_BLOCKING, 55 | logGroup: props.difyClusterLogGroup 56 | }), 57 | environment: TaskEnvironments.getPluginDaemonEnvironment(props, this.region), 58 | secrets: TaskEnvironments.getPluginDaemonSecretEnvironment(props) 59 | }) 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /lib/task-definitions/dify-sandbox.ts: -------------------------------------------------------------------------------- 1 | import { NestedStack } from "aws-cdk-lib"; 2 | import { AppProtocol, AwsLogDriverMode, Compatibility, ContainerImage, CpuArchitecture, LogDriver, OperatingSystemFamily, Protocol, TaskDefinition } from "aws-cdk-lib/aws-ecs"; 3 | import { Construct } from "constructs"; 4 | import { DifyTaskDefinitionStackProps } from "./props"; 5 | import { TaskEnvironments } from "./task-environments"; 6 | 7 | export class DifySandboxTaskDefinitionStack extends NestedStack { 8 | 9 | static readonly DIFY_SANDBOX_PORT = 8194 10 | 11 | static readonly SANDBOX_PORT_MAPPING_NAME = "serverless-dify-sandbox-8194-tcp" 12 | 13 | public readonly definition: TaskDefinition 14 | 15 | constructor(scope: Construct, id: string, props: DifyTaskDefinitionStackProps) { 16 | super(scope, id, props); 17 | this.definition = new TaskDefinition(this, "DifySandboxTaskDefinitionStack", { 18 | taskRole: props.difyTaskRole, 19 | executionRole: props.difyTaskRole, 20 | compatibility: Compatibility.EC2_AND_FARGATE, 21 | runtimePlatform: { operatingSystemFamily: OperatingSystemFamily.LINUX, cpuArchitecture: CpuArchitecture.X86_64 }, 22 | cpu: '256', 23 | memoryMiB: '512', 24 | }) 25 | 26 | this.definition.addContainer("sandbox", { 27 | containerName: "sandbox", 28 | image: ContainerImage.fromRegistry(props.difyImage.sandbox), 29 | portMappings: [ 30 | { 31 | containerPort: DifySandboxTaskDefinitionStack.DIFY_SANDBOX_PORT, 32 | hostPort: DifySandboxTaskDefinitionStack.DIFY_SANDBOX_PORT, 33 | name: DifySandboxTaskDefinitionStack.SANDBOX_PORT_MAPPING_NAME, 34 | appProtocol: AppProtocol.http, 35 | protocol: Protocol.TCP 36 | } 37 | ], 38 | logging: LogDriver.awsLogs({ 39 | streamPrefix: "sandbox", 40 | mode: AwsLogDriverMode.NON_BLOCKING, 41 | logGroup: props.difyClusterLogGroup 42 | }), 43 | environment: TaskEnvironments.getSandboxEnvironment(), 44 | secrets: TaskEnvironments.getSandboxSecretEnvironment(props) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/task-definitions/dify-web.ts: -------------------------------------------------------------------------------- 1 | import { NestedStack } from "aws-cdk-lib"; 2 | import { AppProtocol, AwsLogDriverMode, Compatibility, ContainerImage, CpuArchitecture, LogDriver, NetworkMode, OperatingSystemFamily, Protocol, TaskDefinition } from "aws-cdk-lib/aws-ecs"; 3 | import { ManagedPolicy, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam"; 4 | import { Construct } from "constructs"; 5 | import { DifyTaskDefinitionStackProps } from "./props"; 6 | import { TaskEnvironments } from "./task-environments"; 7 | 8 | export class DifyWebTaskDefinitionStack extends NestedStack { 9 | 10 | public readonly definition: TaskDefinition 11 | 12 | static readonly DIFY_WEB_PORT: number = 3000 13 | 14 | static readonly HEALTHY_ENDPOINT = "/apps" 15 | 16 | constructor(scope: Construct, id: string, props: DifyTaskDefinitionStackProps) { 17 | super(scope, id, props) 18 | 19 | const taskRole = new Role(this, 'ServerlessDifyClusterWebTaskRole', { 20 | assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), 21 | }) 22 | taskRole.addToPrincipalPolicy(new PolicyStatement({ 23 | actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'], 24 | resources: ['*'] 25 | })) 26 | taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryPullOnly')) 27 | props.fileStore.bucket.grantReadWrite(taskRole) 28 | 29 | this.definition = new TaskDefinition(this, 'DifyWebTaskDefinitionStack', { 30 | taskRole: taskRole, 31 | executionRole: taskRole, 32 | compatibility: Compatibility.EC2_AND_FARGATE, 33 | networkMode: NetworkMode.AWS_VPC, 34 | runtimePlatform: { 35 | operatingSystemFamily: OperatingSystemFamily.LINUX, 36 | cpuArchitecture: CpuArchitecture.X86_64 37 | }, 38 | cpu: '512', 39 | memoryMiB: '1024', 40 | }) 41 | 42 | this.definition.addContainer('web', { 43 | containerName: "main", 44 | essential: true, 45 | image: ContainerImage.fromRegistry(props.difyImage.web), 46 | cpu: 512, 47 | memoryLimitMiB: 1024, 48 | portMappings: [{ 49 | name: "serverless-dify-web-3000-tcp", 50 | containerPort: DifyWebTaskDefinitionStack.DIFY_WEB_PORT, 51 | hostPort: DifyWebTaskDefinitionStack.DIFY_WEB_PORT, 52 | protocol: Protocol.TCP, appProtocol: AppProtocol.http 53 | }], 54 | logging: LogDriver.awsLogs({ 55 | streamPrefix: 'web', 56 | mode: AwsLogDriverMode.NON_BLOCKING, 57 | logGroup: props.difyClusterLogGroup 58 | }), 59 | environment: TaskEnvironments.getWebEnvironment() 60 | }) 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /lib/task-definitions/dify-worker.ts: -------------------------------------------------------------------------------- 1 | import { NestedStack } from "aws-cdk-lib"; 2 | import { AwsLogDriverMode, Compatibility, ContainerImage, CpuArchitecture, LogDriver, NetworkMode, OperatingSystemFamily, TaskDefinition } from "aws-cdk-lib/aws-ecs"; 3 | import { Construct } from "constructs"; 4 | import { DifyTaskDefinitionStackProps } from "./props"; 5 | import { TaskEnvironments } from "./task-environments"; 6 | 7 | export class DifyWorkerTaskDefinitionStack extends NestedStack { 8 | 9 | public readonly definition: TaskDefinition; 10 | 11 | constructor(scope: Construct, id: string, props: DifyTaskDefinitionStackProps) { 12 | super(scope, id, props); 13 | 14 | this.definition = new TaskDefinition(this, 'DifyWorkerTaskDefinitionStack', { 15 | taskRole: props.difyTaskRole, 16 | executionRole: props.difyTaskRole, 17 | compatibility: Compatibility.EC2_AND_FARGATE, 18 | networkMode: NetworkMode.AWS_VPC, 19 | runtimePlatform: { 20 | operatingSystemFamily: OperatingSystemFamily.LINUX, 21 | cpuArchitecture: CpuArchitecture.X86_64 22 | }, 23 | cpu: '1024', 24 | memoryMiB: '2048', 25 | }) 26 | 27 | this.definition.addContainer('worker', { 28 | containerName: "worker", 29 | essential: true, 30 | image: ContainerImage.fromRegistry(props.difyImage.api), 31 | command: ['worker'], 32 | cpu: 512, 33 | memoryLimitMiB: 1024, 34 | environment: TaskEnvironments.getWorkerEnvironment(props, this.region), 35 | secrets: TaskEnvironments.getWorkerSecretEnvironment(props), 36 | logging: LogDriver.awsLogs({ 37 | streamPrefix: 'worker', 38 | mode: AwsLogDriverMode.NON_BLOCKING, 39 | logGroup: props.difyClusterLogGroup, 40 | }), 41 | 42 | }) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /lib/task-definitions/props.ts: -------------------------------------------------------------------------------- 1 | import { StackProps } from "aws-cdk-lib"; 2 | import { SecurityGroup, Vpc } from "aws-cdk-lib/aws-ec2"; 3 | import { ApplicationListener, ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2"; 4 | import { Role } from "aws-cdk-lib/aws-iam"; 5 | import { LogGroup } from "aws-cdk-lib/aws-logs"; 6 | import { Bucket } from "aws-cdk-lib/aws-s3"; 7 | import { Secret } from "aws-cdk-lib/aws-secretsmanager"; 8 | 9 | export interface DifyNetworkProps { vpc: Vpc, taskSecurityGroup: SecurityGroup } 10 | 11 | export interface DifyFileStoreProps { bucket: Bucket } 12 | 13 | export interface DifyVectorStorePgProps { hostname: string, port: number, defaultDatabase: string, secret: Secret } 14 | 15 | export interface DifyMetadataStoreProps { hostname: string, port: number, defaultDatabase: string, secret: Secret } 16 | 17 | export interface DifyRedisProps { hostname: string, port: string } 18 | 19 | export interface DifyCeleryBrokerProps { hostname: string, port: string } 20 | 21 | export interface DifyIngressProps { lb: ApplicationLoadBalancer, listener: ApplicationListener } 22 | 23 | export interface SmtpServerProps { host: string, port: string, username: string, password: string, tls: boolean, fromEmail: string } 24 | 25 | export interface DifyImage { api: string, web: string, sandbox: string, pluginDaemon: string } 26 | 27 | export interface DifyTaskDefinitionStackProps extends StackProps { 28 | 29 | network: DifyNetworkProps 30 | 31 | celeryBroker: DifyCeleryBrokerProps 32 | 33 | redis: DifyRedisProps 34 | 35 | metadataStore: DifyMetadataStoreProps 36 | 37 | vectorStore: DifyVectorStorePgProps 38 | 39 | fileStore: DifyFileStoreProps 40 | 41 | apiSecretKey: Secret 42 | 43 | pluginInnerApiKey: Secret 44 | 45 | pluginDaemonKey: Secret 46 | 47 | sandboxCodeExecutionKey: Secret 48 | 49 | smtp: SmtpServerProps 50 | 51 | difyImage: DifyImage 52 | 53 | difyTaskRole: Role 54 | 55 | difyClusterLogGroup: LogGroup 56 | } -------------------------------------------------------------------------------- /lib/task-definitions/task-environments.ts: -------------------------------------------------------------------------------- 1 | import { Secret } from "aws-cdk-lib/aws-ecs"; 2 | import { DifyStack } from "../dify-stack"; 3 | import { MetadataStoreStack } from "../metadata-store-stack"; 4 | import { VectorStoreStack } from "../vector-store-stack"; 5 | import { DifyApiTaskDefinitionStack } from "./dify-api"; 6 | import { DifyPluginDaemonTaskDefinitionStack } from "./dify-plugin-daemon"; 7 | import { DifySandboxTaskDefinitionStack } from "./dify-sandbox"; 8 | import { DifyTaskDefinitionStackProps } from "./props"; 9 | 10 | export class TaskEnvironments { 11 | 12 | static getSharedEnvironment(props: DifyTaskDefinitionStackProps, region: string): Record { 13 | return { 14 | CONSOLE_API_URL: "", 15 | CONSOLE_WEB_URL: "", 16 | SERVICE_API_URL: "", 17 | APP_API_URL: "", 18 | APP_WEB_URL: "", 19 | FILES_URL: "", 20 | LOG_LEVEL: "INFO", 21 | LOG_FILE: "app/logs/server.log", 22 | LOG_FILE_MAX_SIZE: "20", 23 | LOG_FILE_BACKUP_COUNT: "5", 24 | LOG_DATEFORMAT: "%Y-%m-%d %H:%M:%S", 25 | LOG_TZ: "UTC", 26 | DEBUG: "false", 27 | FLASK_DEBUG: "false", 28 | INIT_PASSWORD: "", 29 | DEPLOY_ENV: "PRODUCTION", 30 | MIGRATION_ENABLED: "true", 31 | FILES_ACCESS_TIMEOUT: "300", 32 | ACCESS_TOKEN_EXPIRE_MINUTES: "60", 33 | REFRESH_TOKEN_EXPIRE_DAYS: "30", 34 | APP_MAX_ACTIVE_REQUESTS: "0", 35 | APP_MAX_EXECUTION_TIME: "1200", 36 | DIFY_BIND_ADDRESS: "0.0.0.0", 37 | DIFY_PORT: "5001", 38 | SERVER_WORKER_AMOUNT: "1", 39 | SERVER_WORKER_CLASS: "gevent", 40 | SERVER_WORKER_CONNECTIONS: "10", 41 | CELERY_WORKER_CLASS: "", 42 | GUNICORN_TIMEOUT: "360", 43 | CELERY_WORKER_AMOUNT: "", 44 | CELERY_AUTO_SCALE: "false", 45 | CELERY_MAX_WORKERS: "", 46 | CELERY_MIN_WORKERS: "", 47 | API_TOOL_DEFAULT_CONNECT_TIMEOUT: "10", 48 | API_TOOL_DEFAULT_READ_TIMEOUT: "60", 49 | SQLALCHEMY_POOL_SIZE: "30", 50 | SQLALCHEMY_POOL_RECYCLE: "3600", 51 | SQLALCHEMY_ECHO: "false", 52 | POSTGRES_MAX_CONNECTIONS: "100", 53 | POSTGRES_SHARED_BUFFERS: "128MB", 54 | POSTGRES_WORK_MEM: "4MB", 55 | POSTGRES_MAINTENANCE_WORK_MEM: "64MB", 56 | POSTGRES_EFFECTIVE_CACHE_SIZE: "4096MB", 57 | 58 | DB_DATABASE: MetadataStoreStack.DEFAULT_DATABASE, 59 | DB_PLUGIN_DATABASE: MetadataStoreStack.DEFAULT_PLUGIN_DATABASE, 60 | 61 | VECTOR_STORE: "pgvector", 62 | PGVECTOR_DATABASE: VectorStoreStack.DEFAULT_DATABASE, 63 | 64 | // REDIS_USERNAME: // redis disabled auth 65 | // REDIS_PASSWORD: // redis disabled auth 66 | REDIS_HOST: props.redis.hostname, 67 | REDIS_PORT: props.redis.port, 68 | REDIS_USE_SSL: "true", 69 | REDIS_DB: "0", 70 | 71 | CELERY_BROKER_URL: `redis://${props.celeryBroker.hostname}:${props.celeryBroker.port}/0`, 72 | BROKER_USE_SSL: "true", 73 | CELERY_USE_SENTINEL: "false", 74 | 75 | WEB_API_CORS_ALLOW_ORIGINS: "*", 76 | CONSOLE_CORS_ALLOW_ORIGINS: "*", 77 | 78 | AWS_REGION: region, 79 | 80 | STORAGE_TYPE: "s3", 81 | // S3_ENDPOINT: "", 82 | S3_REGION: region, 83 | S3_BUCKET_NAME: props.fileStore.bucket.bucketName, 84 | S3_USE_AWS_MANAGED_IAM: "true", 85 | S3_ENDPOINT: `https://s3.${region}.amazonaws.com`, 86 | 87 | MAIL_TYPE: "smtp", 88 | MAIL_DEFAULT_SEND_FROM: props.smtp.fromEmail, 89 | SMTP_SERVER: props.smtp.host, 90 | SMTP_PORT: props.smtp.port.toString(), 91 | SMTP_USERNAME: props.smtp.username, 92 | SMTP_PASSWORD: props.smtp.password, 93 | SMTP_USE_TLS: props.smtp.tls ? "true" : "false", 94 | 95 | INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: "4000", 96 | INVITE_EXPIRY_HOURS: "72", 97 | RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: "5", 98 | 99 | CODE_EXECUTION_ENDPOINT: `http://${DifyStack.DIFY_SANDBOX_SERVICE_DNS_NAME}:${DifySandboxTaskDefinitionStack.DIFY_SANDBOX_PORT}`, 100 | CODE_MAX_NUMBER: "9223372036854775807", 101 | CODE_MIN_NUMBER: "-9223372036854775808", 102 | CODE_MAX_DEPTH: "5", 103 | CODE_MAX_PRECISION: "20", 104 | CODE_MAX_STRING_LENGTH: "80000", 105 | CODE_MAX_STRING_ARRAY_LENGTH: "30", 106 | CODE_MAX_OBJECT_ARRAY_LENGTH: "30", 107 | CODE_MAX_NUMBER_ARRAY_LENGTH: "1000", 108 | CODE_EXECUTION_CONNECT_TIMEOUT: "10", 109 | CODE_EXECUTION_READ_TIMEOUT: "60", 110 | CODE_EXECUTION_WRITE_TIMEOUT: "10", 111 | TEMPLATE_TRANSFORM_MAX_LENGTH: "80000", 112 | 113 | WORKFLOW_MAX_EXECUTION_STEPS: "500", 114 | WORKFLOW_MAX_EXECUTION_TIME: "1200", 115 | WORKFLOW_CALL_MAX_DEPTH: "5", 116 | MAX_VARIABLE_SIZE: "204800", 117 | 118 | WORKFLOW_PARALLEL_DEPTH_LIMIT: "3", 119 | WORKFLOW_FILE_UPLOAD_LIMIT: "10", 120 | HTTP_REQUEST_NODE_MAX_BINARY_SIZE: "10485760", 121 | HTTP_REQUEST_NODE_MAX_TEXT_SIZE: "1048576", 122 | HTTP_REQUEST_NODE_SSL_VERIFY: "True", 123 | 124 | SANDBOX_PORT: "8194", 125 | SANDBOX_GIN_MODE: "release", 126 | SANDBOX_WORKER_TIMEOUT: "15", 127 | SANDBOX_ENABLE_NETWORK: "true", 128 | 129 | TOP_K_MAX_VALUE: "10", 130 | EXPOSE_PLUGIN_DAEMON_PORT: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_PORT.toString(), 131 | PLUGIN_DAEMON_PORT: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT.toString(), 132 | PLUGIN_DAEMON_URL: `http://${DifyStack.DIFY_PLUGIN_DAEMON_SERVICE_DNS_NAME}:${DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_PORT}`, 133 | PLUGIN_MAX_PACKAGE_SIZE: "52428800", 134 | PLUGIN_PPROF_ENABLED: "false", 135 | PLUGIN_DEBUGGING_HOST: "0.0.0.0", 136 | PLUGIN_DEBUGGING_PORT: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT.toString(), 137 | EXPOSE_PLUGIN_DEBUGGING_HOST: DifyStack.DIFY_PLUGIN_DAEMON_SERVICE_DNS_NAME, 138 | EXPOSE_PLUGIN_DEBUGGING_PORT: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT.toString(), 139 | 140 | PLUGIN_DIFY_INNER_API_URL: `http://${DifyStack.DIFY_API_SERVICE_DNS_NAME}:${DifyApiTaskDefinitionStack.DIFY_API_PORT}`, 141 | 142 | // ENDPOINT_URL_TEMPLATE: ${ENDPOINT_URL_TEMPLATE:-http://localhost/e/{hook_id}} 143 | MARKETPLACE_ENABLED: "true", 144 | MARKETPLACE_API_URL: "https://marketplace.dify.ai", 145 | MARKETPLACE_URL: "https://marketplace.dify.ai", 146 | 147 | FORCE_VERIFYING_SIGNATURE: "true", 148 | 149 | PLUGIN_PYTHON_ENV_INIT_TIMEOUT: "120", 150 | PLUGIN_MAX_EXECUTION_TIMEOUT: "600", 151 | // PIP_MIRROR_URL: ${PIP_MIRROR_URL:-} 152 | } 153 | } 154 | 155 | static getSharedSecretEnvironment(props: DifyTaskDefinitionStackProps): Record { 156 | return { 157 | SECRET_KEY: Secret.fromSecretsManager(props.apiSecretKey), 158 | CODE_EXECUTION_API_KEY: Secret.fromSecretsManager(props.sandboxCodeExecutionKey), 159 | PLUGIN_DIFY_INNER_API_KEY: Secret.fromSecretsManager(props.pluginInnerApiKey), 160 | SANDBOX_API_KEY: Secret.fromSecretsManager(props.sandboxCodeExecutionKey), 161 | PLUGIN_DAEMON_KEY: Secret.fromSecretsManager(props.pluginDaemonKey), 162 | 163 | DB_USERNAME: Secret.fromSecretsManager(props.metadataStore.secret, "username"), 164 | DB_PASSWORD: Secret.fromSecretsManager(props.metadataStore.secret, "password"), 165 | DB_HOST: Secret.fromSecretsManager(props.metadataStore.secret, "host"), 166 | DB_PORT: Secret.fromSecretsManager(props.metadataStore.secret, "port"), 167 | 168 | PGVECTOR_HOST: Secret.fromSecretsManager(props.vectorStore.secret, "host"), 169 | PGVECTOR_PORT: Secret.fromSecretsManager(props.vectorStore.secret, "port"), 170 | PGVECTOR_USERNAME: Secret.fromSecretsManager(props.vectorStore.secret, "username"), 171 | PGVECTOR_PASSWORD: Secret.fromSecretsManager(props.vectorStore.secret, "password"), 172 | 173 | } 174 | } 175 | 176 | 177 | static getApiEnvironment(taskDefinitionProps: DifyTaskDefinitionStackProps, region: string): Record { 178 | const env = this.getSharedEnvironment(taskDefinitionProps, region); 179 | return { 180 | ...env, 181 | MODE: "api", 182 | // SENTRY_DSN: "", 183 | // SENTRY_TRACES_SAMPLE_RATE: "1.0", 184 | // SENTRY_PROFILES_SAMPLE_RATE: "1.0", 185 | PLUGIN_REMOTE_INSTALL_HOST: DifyStack.DIFY_PLUGIN_DAEMON_SERVICE_DNS_NAME, 186 | PLUGIN_REMOTE_INSTALL_PORT: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT.toString(), 187 | PLUGIN_MAX_PACKAGE_SIZE: "52428800", 188 | 189 | }; 190 | } 191 | 192 | static getApiSecretEnvironment(taskDefinitionProps: DifyTaskDefinitionStackProps): Record { 193 | const env = this.getSharedSecretEnvironment(taskDefinitionProps); 194 | return { 195 | ...env, 196 | INNER_API_KEY_FOR_PLUGIN: Secret.fromSecretsManager(taskDefinitionProps.pluginInnerApiKey) 197 | } 198 | } 199 | 200 | static getWorkerEnvironment(taskDefinitionProps: DifyTaskDefinitionStackProps, region: string): Record { 201 | const env = this.getApiEnvironment(taskDefinitionProps, region); 202 | return { 203 | ...env, 204 | MODE: "worker", 205 | // SENTRY_DSN: "", 206 | // SENTRY_TRACES_SAMPLE_RATE: "1.0", 207 | // SENTRY_PROFILES_SAMPLE_RATE: "1.0", 208 | PLUGIN_MAX_PACKAGE_SIZE: "52428800", 209 | }; 210 | } 211 | 212 | static getWorkerSecretEnvironment(taskDefinitionProps: DifyTaskDefinitionStackProps): Record { 213 | const env = this.getApiSecretEnvironment(taskDefinitionProps); 214 | return { 215 | ...env, 216 | INNER_API_KEY_FOR_PLUGIN: Secret.fromSecretsManager(taskDefinitionProps.pluginInnerApiKey) 217 | } 218 | } 219 | 220 | static getWebEnvironment(): Record { 221 | return { 222 | EDITION: "SELF_HOSTED", 223 | 224 | CONSOLE_API_URL: "", 225 | APP_API_URL: "", 226 | // SENTRY_DSN: ${ WEB_SENTRY_DSN: -} 227 | NEXT_TELEMETRY_DISABLED: "", 228 | TEXT_GENERATION_TIMEOUT_MS: "60000", 229 | CSP_WHITELIST: "", 230 | TOP_K_MAX_VALUE: "", 231 | INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: "", 232 | PM2_INSTANCES: "2", 233 | LOOP_NODE_MAX_COUNT: "100", 234 | MAX_TOOLS_NUM: "10", 235 | MAX_PARALLEL_LIMIT: "10", 236 | 237 | MARKETPLACE_API_URL: "https://marketplace.dify.ai", 238 | MARKETPLACE_URL: "https://marketplace.dify.ai" 239 | } 240 | } 241 | 242 | static getSandboxEnvironment(): Record { 243 | return { 244 | GIN_MODE: "release", 245 | WORKER_TIMEOUT: "15", 246 | ENABLE_NETWORK: "true", 247 | SANDBOX_PORT: "8194" 248 | }; 249 | } 250 | 251 | static getSandboxSecretEnvironment(props: DifyTaskDefinitionStackProps): Record { 252 | return { 253 | API_KEY: Secret.fromSecretsManager(props.sandboxCodeExecutionKey) 254 | } 255 | } 256 | 257 | static getPluginDaemonEnvironment(props: DifyTaskDefinitionStackProps, region: string): Record { 258 | const env = this.getSharedEnvironment(props, region) 259 | return { 260 | ...env, 261 | GIN_MODE: 'release', 262 | PLATFORM: 'local', 263 | DB_DATABASE: MetadataStoreStack.DEFAULT_PLUGIN_DATABASE, 264 | SERVER_PORT: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_PORT.toString(), 265 | MAX_PLUGIN_PACKAGE_SIZE: "52428800", 266 | MAX_BUNDLE_PACKAGE_SIZE: "52428800", 267 | 268 | PPROF_ENABLED: "false", 269 | DIFY_INNER_API_URL: `http://${DifyStack.DIFY_API_SERVICE_DNS_NAME}:${DifyApiTaskDefinitionStack.DIFY_API_PORT}`, 270 | PLUGIN_REMOTE_INSTALLING_ENABLED: 'true', 271 | PLUGIN_REMOTE_INSTALLING_HOST: DifyStack.DIFY_PLUGIN_DAEMON_SERVICE_DNS_NAME, 272 | PLUGIN_REMOTE_INSTALLING_PORT: DifyPluginDaemonTaskDefinitionStack.DIFY_PLUGIN_DAEMON_DEBUG_PORT.toString(), 273 | PLUGIN_STORAGE_TYPE: 'aws_s3', 274 | PLUGIN_STORAGE_OSS_BUCKET: props.fileStore.bucket.bucketName, 275 | PLUGIN_INSTALLED_PATH: "plugin", 276 | PLUGIN_WORKING_PATH: "/app/storage/cwd", 277 | 278 | ROUTINE_POOL_SIZE: '10000', 279 | LIFETIME_COLLECTION_HEARTBEAT_INTERVAL: '5', 280 | LIFETIME_COLLECTION_GC_INTERVAL: '60', 281 | LIFETIME_STATE_GC_INTERVAL: '300', 282 | DIFY_INVOCATION_CONNECTION_IDLE_TIMEOUT: '120', 283 | 284 | FORCE_VERIFYING_SIGNATURE: "true", 285 | PYTHON_ENV_INIT_TIMEOUT: "120", 286 | PLUGIN_MAX_EXECUTION_TIMEOUT: "600", 287 | PIP_MIRROR_URL: "" 288 | }; 289 | } 290 | 291 | static getPluginDaemonSecretEnvironment(props: DifyTaskDefinitionStackProps): Record { 292 | const env = this.getSharedSecretEnvironment(props) 293 | return { 294 | ...env, 295 | SERVER_KEY: Secret.fromSecretsManager(props.pluginDaemonKey), 296 | DIFY_INNER_API_KEY: Secret.fromSecretsManager(props.pluginInnerApiKey) 297 | } 298 | } 299 | 300 | } 301 | -------------------------------------------------------------------------------- /lib/vector-store-stack.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; 2 | import { SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2"; 3 | import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam"; 4 | import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; 5 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 6 | import { RetentionDays } from "aws-cdk-lib/aws-logs"; 7 | import { AuroraPostgresEngineVersion, ClusterInstance, Credentials, DatabaseCluster, DatabaseClusterEngine, SubnetGroup } from "aws-cdk-lib/aws-rds"; 8 | import { Secret } from "aws-cdk-lib/aws-secretsmanager"; 9 | import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources"; 10 | import { Construct } from "constructs"; 11 | import { DifyVectorStorePgProps } from "./task-definitions/props"; 12 | import path = require("path"); 13 | 14 | export interface VectorStoreStackProps extends StackProps { 15 | 16 | vpc: Vpc 17 | 18 | sg: SecurityGroup 19 | } 20 | 21 | export class VectorStoreStack extends Stack { 22 | 23 | static readonly PORT: number = 5432 24 | 25 | static readonly DEFAULT_DATABASE: string = 'dify' 26 | 27 | public readonly cluster: DatabaseCluster 28 | 29 | public readonly secret: Secret 30 | 31 | private readonly sqlExecFunction: Function 32 | 33 | constructor(scope: Construct, id: string, props: VectorStoreStackProps) { 34 | super(scope, id, props) 35 | 36 | const subnetGroup = new SubnetGroup(this, "VectorStoreSubnetGroup", { 37 | description: 'Used for serverless-dify vector store', 38 | vpc: props.vpc, 39 | removalPolicy: RemovalPolicy.DESTROY, 40 | vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS }, 41 | }); 42 | 43 | this.secret = new Secret(this, "VectorStoreDatabaseSecret", { 44 | description: "Used for serverless-dify vector store", 45 | generateSecretString: { 46 | secretStringTemplate: JSON.stringify({ username: "XXXXXXX" }), 47 | generateStringKey: "password", 48 | excludePunctuation: true 49 | } 50 | }) 51 | 52 | this.cluster = new DatabaseCluster(this, 'VectorStoreDatabaseCluster', { 53 | vpc: props.vpc, 54 | securityGroups: [props.sg], 55 | port: VectorStoreStack.PORT, 56 | subnetGroup: subnetGroup, 57 | clusterIdentifier: 'serverless-dify-vector-store', 58 | engine: DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.VER_16_6 }), 59 | writer: ClusterInstance.serverlessV2("DifyVectorStore"), 60 | cloudwatchLogsRetention: RetentionDays.ONE_WEEK, 61 | serverlessV2MinCapacity: 0, 62 | serverlessV2MaxCapacity: 2, 63 | iamAuthentication: true, 64 | // enableDataApi: true, 65 | credentials: Credentials.fromSecret(this.secret), 66 | defaultDatabaseName: VectorStoreStack.DEFAULT_DATABASE, 67 | }) 68 | 69 | this.sqlExecFunction = new NodejsFunction(this, 'SqlExecFunction', { 70 | runtime: Runtime.NODEJS_22_X, 71 | handler: 'index.handler', 72 | memorySize: 256, 73 | environment: { SECRET_ARN: this.secret.secretArn }, 74 | code: Code.fromAsset(path.join(__dirname, './aurorapg-run-query/')), 75 | securityGroups: [props.sg], 76 | vpc: props.vpc, 77 | vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS }, 78 | logRetention: RetentionDays.ONE_DAY 79 | }) 80 | 81 | this.secret.grantRead(this.sqlExecFunction) 82 | 83 | const query = this.enablePgvectorExtension() 84 | this.cluster.secret?.grantRead(query) 85 | this.cluster.grantDataApiAccess(query) 86 | query.node.addDependency(this.cluster) 87 | } 88 | 89 | 90 | private enablePgvectorExtension() { 91 | const query = new AwsCustomResource(this, 'Query-EnablePgvectorExtension', { 92 | onUpdate: { 93 | service: 'Lambda', 94 | action: 'invoke', 95 | parameters: { 96 | FunctionName: this.sqlExecFunction.functionName, 97 | InvocationType: 'RequestResponse', 98 | Payload: JSON.stringify({ "query": "CREATE EXTENSION IF NOT EXISTS vector" }) 99 | }, 100 | physicalResourceId: PhysicalResourceId.of('EnablePgvectorExtension') 101 | }, 102 | 103 | policy: AwsCustomResourcePolicy.fromStatements([ 104 | new PolicyStatement({ 105 | effect: Effect.ALLOW, 106 | actions: ['lambda:InvokeFunction'], 107 | resources: [this.sqlExecFunction.functionArn] 108 | }) 109 | ]) 110 | }) 111 | 112 | return query 113 | } 114 | 115 | public exportProps(): DifyVectorStorePgProps { 116 | return { 117 | hostname: this.cluster.clusterEndpoint.hostname, 118 | port: VectorStoreStack.PORT, 119 | defaultDatabase: VectorStoreStack.DEFAULT_DATABASE, 120 | secret: this.secret 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-dify", 3 | "version": "0.1.0", 4 | "bin": { 5 | "serverless-dify": "bin/serverless-dify.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.12", 15 | "@types/node": "20.14.9", 16 | "aws-cdk": "2.151.0", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.5", 19 | "typescript": "~5.5.3" 20 | }, 21 | "dependencies": { 22 | "aws-cdk-lib": "^2.189.1", 23 | "constructs": "^10.0.0", 24 | "dotenv": "^16.4.7", 25 | "source-map-support": "^0.5.21" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-serverless-dify-stack/b97b22d5fe4672813dea5fa767ee21af135a038e/resources/architecture.png -------------------------------------------------------------------------------- /resources/serverless-dify.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /test/serverless-dify.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as ServerlessDify from '../lib/serverless-dify-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/serverless-dify-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new ServerlessDify.ServerlessDifyStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------