Easy AWS cloud deployment for private instances via CloudFormation template (#52)

* wip

* fix file ref

* update dockerfile

* mute chown

* add build script for AWS CF template construction
add comment about script and AWS deployment

* move aws stuff into its own folder

* edit readme
This commit is contained in:
Timothy Carambat 2023-06-14 10:34:22 -07:00 committed by GitHub
parent caea4ea1b2
commit 4dd1887bd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 382 additions and 2 deletions

1
.gitignore vendored
View File

@ -6,4 +6,5 @@ node_modules
__pycache__ __pycache__
v-env v-env
.DS_Store .DS_Store
aws_cf_deploy_anything_llm.json

View File

@ -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.

View File

@ -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"
}
}
}
}

View File

@ -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();

View File

@ -4,6 +4,7 @@ FROM ubuntu:jammy-20230522 AS base
# Build arguments # Build arguments
ARG ARG_UID ARG ARG_UID
ARG ARG_GID ARG ARG_GID
ARG ARG_CLOUD_BUILD=0 # Default to local docker build
ARG ARCH=amd64 # Default to amd64 if not provided ARG ARCH=amd64 # Default to amd64 if not provided
# Install system dependencies # Install system dependencies
@ -89,6 +90,17 @@ EXPOSE 3001
HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \ HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \
CMD /bin/bash /usr/local/bin/docker-healthcheck.sh || exit 1 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 # Run the server
ENTRYPOINT ["docker-entrypoint.sh"] ENTRYPOINT ["docker-entrypoint.sh"]

View File

@ -16,6 +16,7 @@ services:
args: args:
ARG_UID: ${UID} ARG_UID: ${UID}
ARG_GID: ${GID} ARG_GID: ${GID}
ARG_CLOUD_BUILD: ${CLOUD_BUILD}
ARCH: ${ARCH:-amd64} ARCH: ${ARCH:-amd64}
volumes: volumes:
- "../server/storage:/app/server/storage" - "../server/storage:/app/server/storage"

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

View File

@ -15,7 +15,11 @@
"dev:server": "cd server && yarn dev", "dev:server": "cd server && yarn dev",
"dev:frontend": "cd frontend && yarn start", "dev:frontend": "cd frontend && yarn start",
"prod:server": "cd server && 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"
}
} }

8
yarn.lock Normal file
View File

@ -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==