diff --git a/.gitignore b/.gitignore index 8ab7553f6..0725f47c0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ node_modules __pycache__ v-env .DS_Store +aws_cf_deploy_anything_llm.json diff --git a/aws/cloudformation/DEPLOY.md b/aws/cloudformation/DEPLOY.md new file mode 100644 index 000000000..7f1ee382e --- /dev/null +++ b/aws/cloudformation/DEPLOY.md @@ -0,0 +1,54 @@ +# How to deploy a private AnythingLLM instance on AWS + +With an AWS account you can easily deploy a private AnythingLLM instance on AWS. This will create a url that you can access from any browser over HTTP (HTTPS not supported). This single instance will run on your own keys and they will not be exposed - however if you want your instance to be protected it is highly recommend that you set the `AUTH_TOKEN` and `JWT_SECRET` variables in the `docker/` ENV. + +[Refer to .env.example](../../docker/HOW_TO_USE_DOCKER.md) for data format. + +The output of this cloudformation stack will be: +- 1 EC2 Instance +- 1 Security Group with 0.0.0.0/0 access on Ports 22 & 3001 +- 1 EC2 Instance Volume `gb2` of 10Gib minimum + +**Requirements** +- An AWS account with billing information. + - AnythingLLM can run within the free tier using a t2.micro and 10Gib SSD hard disk volume +- `.env` file that is filled out with your settings and set up in the `docker/` folder + +## How to deploy on AWS + +1. Generate your specific cloudformation document by running `yarn generate:cloudformation` from the project root directory. +2. Log in to your AWS account +3. Open [CloudFormation](https://us-west-1.console.aws.amazon.com/cloudformation/home) +4. Ensure you are deploying in a geographic zone that is nearest to your physical location to reduce latency. +5. Click `Create Stack` + +![Create Stack](/images/screenshots/create_stack.png) + +6. Upload your `aws_cf_deploy_anything_llm.json` to the stack + +![Upload Stack](/images/screenshots/upload.png) + +7. Click `Next` and give your stack a name. This is superficial. +8. No other changes are needed, just proceed though each step +9. Click `Submit` +10. Wait for stack events to finish and be marked as `Completed` +11. View `Outputs` tab. + +![Stack Output](/images/screenshots/cf_outputs.png) + +## Please read this notice before submitting issues about your deployment + +**Note:** +Your instance will not be available instantly. Depending on the instance size you launched with it can take anywhere from 10-20 minutes to fully boot up. + +If you want to check the instances progress, navigate to [your deployed EC2 instances](https://us-west-1.console.aws.amazon.com/ec2/home) and connect to your instance via SSH in browser. + +Once connected run `sudo tail -f /var/log/cloud-init-output.log` and wait for the file to conclude deployment of the docker image. +You should see an output like this +``` +[+] Running 2/2 + ⠿ Network docker_anything-llm Created + ⠿ Container anything-llm Started +``` + +Additionally, your use of this deployment process means you are responsible for any costs of these AWS resources fully. \ No newline at end of file diff --git a/aws/cloudformation/cf_template.template b/aws/cloudformation/cf_template.template new file mode 100644 index 000000000..1aa2d543c --- /dev/null +++ b/aws/cloudformation/cf_template.template @@ -0,0 +1,244 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Create a stack that runs AnythingLLM on a single instance", + "Parameters": { + "InstanceType": { + "Description": "EC2 instance type", + "Type": "String", + "Default": "t2.micro" + }, + "InstanceVolume": { + "Description": "Storage size of disk on Instance in GB", + "Type": "Number", + "Default": 10, + "MinValue": 2 + } + }, + "Resources": { + "AnythingLLMInstance": { + "Type": "AWS::EC2::Instance", + "Properties": { + "ImageId": { + "Fn::FindInMap": [ + "Region2AMI", + { + "Ref": "AWS::Region" + }, + "AMI" + ] + }, + "InstanceType": { + "Ref": "InstanceType" + }, + "SecurityGroupIds": [ + { + "Ref": "AnythingLLMInstanceSecurityGroup" + } + ], + "BlockDeviceMappings": [ + { + "DeviceName": { + "Fn::FindInMap": [ + "Region2AMI", + { + "Ref": "AWS::Region" + }, + "RootDeviceName" + ] + }, + "Ebs": { + "VolumeSize": { + "Ref": "InstanceVolume" + } + } + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "Content-Type: multipart/mixed; boundary=\"//\"\n", + "MIME-Version: 1.0\n", + "\n", + "--//\n", + "Content-Type: text/cloud-config; charset=\"us-ascii\"\n", + "MIME-Version: 1.0\n", + "Content-Transfer-Encoding: 7bit\n", + "Content-Disposition: attachment; filename=\"cloud-config.txt\"\n", + "\n", + "\n", + "#cloud-config\n", + "cloud_final_modules:\n", + "- [scripts-user, always]\n", + "\n", + "\n", + "--//\n", + "Content-Type: text/x-shellscript; charset=\"us-ascii\"\n", + "MIME-Version: 1.0\n", + "Content-Transfer-Encoding: 7bit\n", + "Content-Disposition: attachment; filename=\"userdata.txt\"\n", + "\n", + "\n", + "#!/bin/bash\n", + "# check output of userdata script with sudo tail -f /var/log/cloud-init-output.log\n", + "sudo yum install docker -y\n", + "sudo usermod -a -G docker ec2-user\n", + "curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose\n", + "sudo chmod +x /usr/local/bin/docker-compose\n", + "sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose\n", + "sudo systemctl enable docker\n", + "sudo systemctl start docker\n", + "sudo yum install git -y\n", + "git clone -b cloud-deploy https://github.com/Mintplex-Labs/anything-llm.git /home/ec2-user/anything-llm\n", + "cd /home/ec2-user/anything-llm/docker\n", + "cat >> .env << \"END\"\n", + "!SUB::USER::CONTENT!", + "UID=\"1000\"\n", + "GID=\"1000\"\n", + "CLOUD_BUILD=1\n", + "END\n", + "cd ../frontend\n", + "rm -rf .env.production\n", + "cat >> .env.production << \"END\"\n", + "GENERATE_SOURCEMAP=true\n", + "VITE_API_BASE=\"/api\"\n", + "END\n", + "sudo docker-compose -f /home/ec2-user/anything-llm/docker/docker-compose.yml up -d\n", + "\n", + "--//--\n" + ] + ] + } + } + } + }, + "AnythingLLMInstanceSecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "AnythingLLm Instance Security Group", + "SecurityGroupIngress": [ + { + "IpProtocol": "tcp", + "FromPort": "22", + "ToPort": "22", + "CidrIp": "0.0.0.0/0" + }, + { + "IpProtocol": "tcp", + "FromPort": "3001", + "ToPort": "3001", + "CidrIp": "0.0.0.0/0" + }, + { + "IpProtocol": "tcp", + "FromPort": "3001", + "ToPort": "3001", + "CidrIpv6": "::/0" + } + ] + } + } + }, + "Outputs": { + "ServerIp": { + "Description": "IP address of the AnythingLLM instance", + "Value": { + "Fn::GetAtt": [ + "AnythingLLMInstance", + "PublicIp" + ] + } + }, + "ServerURL": { + "Description": "URL of the AnythingLLM server", + "Value": { + "Fn::Join": [ + "", + [ + "http://", + { + "Fn::GetAtt": [ + "AnythingLLMInstance", + "PublicIp" + ] + }, + ":3001" + ] + ] + } + } + }, + "Mappings": { + "Region2AMI": { + "ap-south-1": { + "AMI": "ami-0e6329e222e662a52", + "RootDeviceName": "/dev/xvda" + }, + "eu-north-1": { + "AMI": "ami-08c308b1bb265e927", + "RootDeviceName": "/dev/xvda" + }, + "eu-west-3": { + "AMI": "ami-069d1ea6bc64443f0", + "RootDeviceName": "/dev/xvda" + }, + "eu-west-2": { + "AMI": "ami-06a566ca43e14780d", + "RootDeviceName": "/dev/xvda" + }, + "eu-west-1": { + "AMI": "ami-0a8dc52684ee2fee2", + "RootDeviceName": "/dev/xvda" + }, + "ap-northeast-3": { + "AMI": "ami-0c8a89b455fae8513", + "RootDeviceName": "/dev/xvda" + }, + "ap-northeast-2": { + "AMI": "ami-0ff56409a6e8ea2a0", + "RootDeviceName": "/dev/xvda" + }, + "ap-northeast-1": { + "AMI": "ami-0ab0bbbd329f565e6", + "RootDeviceName": "/dev/xvda" + }, + "ca-central-1": { + "AMI": "ami-033c256a10931f206", + "RootDeviceName": "/dev/xvda" + }, + "sa-east-1": { + "AMI": "ami-0dabf4dab6b183eef", + "RootDeviceName": "/dev/xvda" + }, + "ap-southeast-1": { + "AMI": "ami-0dc5785603ad4ff54", + "RootDeviceName": "/dev/xvda" + }, + "ap-southeast-2": { + "AMI": "ami-0c5d61202c3b9c33e", + "RootDeviceName": "/dev/xvda" + }, + "eu-central-1": { + "AMI": "ami-004359656ecac6a95", + "RootDeviceName": "/dev/xvda" + }, + "us-east-1": { + "AMI": "ami-0cff7528ff583bf9a", + "RootDeviceName": "/dev/xvda" + }, + "us-east-2": { + "AMI": "ami-02238ac43d6385ab3", + "RootDeviceName": "/dev/xvda" + }, + "us-west-1": { + "AMI": "ami-01163e76c844a2129", + "RootDeviceName": "/dev/xvda" + }, + "us-west-2": { + "AMI": "ami-0ceecbb0f30a902a6", + "RootDeviceName": "/dev/xvda" + } + } + } +} \ No newline at end of file diff --git a/aws/cloudformation/generate.mjs b/aws/cloudformation/generate.mjs new file mode 100644 index 000000000..374a9e1b1 --- /dev/null +++ b/aws/cloudformation/generate.mjs @@ -0,0 +1,56 @@ +// Note (tcarambat) This script should be executed from root via the `yarn generate::cloudformation` command only. +// This script will copy your current Docker .env settings being used into a slightly custom AWS CloudFormation template +// that you can upload and deploy on AWS in a single click! +// Recommended settings are already defined in the template but you can modify them as needed. +// AnythingLLM can run within the free tier services of AWS (t2.micro w/10GB of storage) +// +// This will deploy a fully public AnythingLLM so if you do not want anyone to access it please set the AUTH_TOKEN & JWT_SECRET envs +// before running this script. You can still run the collector scripts on AWS so no FTP or file uploads are required. +// Your documents and data do not leave your AWS instance when you host in the cloud this way. + +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import path, { dirname } from 'path'; +import { exit } from 'process'; +import chalk from 'chalk'; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPLACEMENT_KEY = '!SUB::USER::CONTENT!' + +const envPath = path.resolve(__dirname, `../../docker/.env`) +const envFileExists = fs.existsSync(envPath); + +if (!envFileExists) { + console.log(chalk.redBright('[ABORT]'), 'You do not have an .env file in your ./docker/ folder. You need to create it first.'); + console.log('You can start by running', chalk.cyan('cp -n ./docker/.env.example ./docker/.env')) + exit(1); +} + +// Remove comments +// Remove UID,GID,etc +// Remove empty strings +// Split into array +const settings = fs.readFileSync(envPath, "utf8") + .replace(/^#.*\n?/gm, '') + .replace(/^UID.*\n?/gm, '') + .replace(/^GID.*\n?/gm, '') + .replace(/^CLOUD_BUILD.*\n?/gm, '') + .replace(/^\s*\n/gm, "") + .split('\n') + .filter((i) => !!i) + + +const templatePath = path.resolve(__dirname, `cf_template.template`); +const templateString = fs.readFileSync(templatePath, "utf8"); +const template = JSON.parse(templateString); + +const cmdIdx = template.Resources.AnythingLLMInstance.Properties.UserData['Fn::Base64']['Fn::Join'][1].findIndex((cmd) => cmd === REPLACEMENT_KEY) +template.Resources.AnythingLLMInstance.Properties.UserData['Fn::Base64']['Fn::Join'][1].splice(cmdIdx, 1, ...settings); + +const output = path.resolve(__dirname, `aws_cf_deploy_anything_llm.json`); +fs.writeFileSync(output, JSON.stringify(template, null, 2), "utf8"); + +console.log(chalk.greenBright('[SUCCESS]'), 'Deploy AnythingLLM on AWS CloudFormation using your template document.'); +console.log(chalk.greenBright('File Created:'), 'aws_cf_deploy_anything_llm.json in aws/cloudformation directory.'); +console.log(chalk.blueBright('[INFO]'), 'Refer to aws/cloudformation/DEPLOY.md for how to use this file.'); + +exit(); \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 9db119b7e..0c1b8cda8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,6 +4,7 @@ FROM ubuntu:jammy-20230522 AS base # Build arguments ARG ARG_UID ARG ARG_GID +ARG ARG_CLOUD_BUILD=0 # Default to local docker build ARG ARCH=amd64 # Default to amd64 if not provided # Install system dependencies @@ -89,6 +90,17 @@ EXPOSE 3001 HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \ CMD /bin/bash /usr/local/bin/docker-healthcheck.sh || exit 1 +# Docker will still install deps as root so need to force chown +# or else +USER root +RUN if [ "$ARG_CLOUD_BUILD" = 1 ] ; then \ + echo "Reowning all files as user!" && \ + mkdir -p app/server/storage app/server/storage/documents app/server/storage/vector-cache app/server/storage/lancedb && \ + touch anythingllm.db && \ + chown -R anythingllm:anythingllm /app/collector /app/server; \ + fi +USER anythingllm + # Run the server ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 48232ecad..3500f77b8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,6 +16,7 @@ services: args: ARG_UID: ${UID} ARG_GID: ${GID} + ARG_CLOUD_BUILD: ${CLOUD_BUILD} ARCH: ${ARCH:-amd64} volumes: - "../server/storage:/app/server/storage" diff --git a/images/screenshots/cf_outputs.png b/images/screenshots/cf_outputs.png new file mode 100644 index 000000000..0a4d9cbd6 Binary files /dev/null and b/images/screenshots/cf_outputs.png differ diff --git a/images/screenshots/create_stack.png b/images/screenshots/create_stack.png new file mode 100644 index 000000000..0ad862f1e Binary files /dev/null and b/images/screenshots/create_stack.png differ diff --git a/images/screenshots/upload.png b/images/screenshots/upload.png new file mode 100644 index 000000000..d4b6e94a7 Binary files /dev/null and b/images/screenshots/upload.png differ diff --git a/package.json b/package.json index 151dcd459..4727ff344 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,11 @@ "dev:server": "cd server && yarn dev", "dev:frontend": "cd frontend && yarn start", "prod:server": "cd server && yarn start", - "prod:frontend": "cd frontend && yarn build" + "prod:frontend": "cd frontend && yarn build", + "generate:cloudformation": "node aws/cloudformation/generate.mjs" }, - "private": false + "private": false, + "devDependencies": { + "chalk": "^5.2.0" + } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..b0812b7a7 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +chalk@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" + integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==