From c24898da61ce375e4cd77bcb8bd09615dd297b78 Mon Sep 17 00:00:00 2001 From: Jay V Date: Sat, 5 Oct 2024 15:46:41 -0400 Subject: [PATCH] docs: express redis --- examples/aws-bun-elysia/sst.config.ts | 2 +- .../aws-express-file-upload/.dockerignore | 1 + examples/aws-express-file-upload/.gitignore | 3 + examples/aws-express-file-upload/Dockerfile | 10 ++ examples/aws-express-file-upload/index.mjs | 62 +++++++++ examples/aws-express-file-upload/package.json | 20 +++ examples/aws-express-file-upload/sst-env.d.ts | 21 +++ .../aws-express-file-upload/sst.config.ts | 68 ++++++++++ .../aws-express-file-upload/tsconfig.json | 1 + examples/aws-express/.dockerignore | 4 + examples/aws-express/index.mjs | 64 +++------ examples/aws-express/package.json | 14 +- examples/aws-express/sst-env.d.ts | 10 +- examples/aws-express/sst.config.ts | 10 +- www/src/content/docs/docs/examples.mdx | 62 ++++++++- .../content/docs/docs/start/aws/express.mdx | 125 +++++++----------- 16 files changed, 336 insertions(+), 141 deletions(-) create mode 100644 examples/aws-express-file-upload/.dockerignore create mode 100644 examples/aws-express-file-upload/.gitignore create mode 100644 examples/aws-express-file-upload/Dockerfile create mode 100644 examples/aws-express-file-upload/index.mjs create mode 100644 examples/aws-express-file-upload/package.json create mode 100644 examples/aws-express-file-upload/sst-env.d.ts create mode 100644 examples/aws-express-file-upload/sst.config.ts create mode 100644 examples/aws-express-file-upload/tsconfig.json diff --git a/examples/aws-bun-elysia/sst.config.ts b/examples/aws-bun-elysia/sst.config.ts index 7e4efad77..ee5179f85 100644 --- a/examples/aws-bun-elysia/sst.config.ts +++ b/examples/aws-bun-elysia/sst.config.ts @@ -39,7 +39,7 @@ * curl http://localhost:3000/latest * ``` * - * Finally, you can deploy it using `bun sst deploy`. + * Finally, you can deploy it using `bun sst deploy --stage production`. */ export default $config({ app(input) { diff --git a/examples/aws-express-file-upload/.dockerignore b/examples/aws-express-file-upload/.dockerignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/examples/aws-express-file-upload/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/aws-express-file-upload/.gitignore b/examples/aws-express-file-upload/.gitignore new file mode 100644 index 000000000..cc54d25a1 --- /dev/null +++ b/examples/aws-express-file-upload/.gitignore @@ -0,0 +1,3 @@ + +# sst +.sst diff --git a/examples/aws-express-file-upload/Dockerfile b/examples/aws-express-file-upload/Dockerfile new file mode 100644 index 000000000..53718c02f --- /dev/null +++ b/examples/aws-express-file-upload/Dockerfile @@ -0,0 +1,10 @@ +FROM node:18-bullseye-slim + +WORKDIR /app/ + +COPY package.json /app +RUN npm install + +COPY index.mjs /app + +ENTRYPOINT ["node", "index.mjs"] diff --git a/examples/aws-express-file-upload/index.mjs b/examples/aws-express-file-upload/index.mjs new file mode 100644 index 000000000..71564dd77 --- /dev/null +++ b/examples/aws-express-file-upload/index.mjs @@ -0,0 +1,62 @@ +import multer from "multer"; +import express from "express"; +import { Resource } from "sst"; +import { Upload } from "@aws-sdk/lib-storage"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { + S3Client, + GetObjectCommand, + ListObjectsV2Command, +} from "@aws-sdk/client-s3"; + +const PORT = 80; + +const app = express(); +const s3 = new S3Client({}); +const upload = multer({ storage: multer.memoryStorage() }); + +app.get("/", async (req, res) => { + res.send("Hello World!"); +}); + +app.post("/", upload.single("file"), async (req, res) => { + const file = req.file; + const params = { + Bucket: Resource.MyBucket.name, + Key: file.originalname, + Body: file.buffer, + }; + + const upload = new Upload({ + params, + client: s3, + }); + + await upload.done(); + + res.status(200).send("File uploaded successfully."); +}); + +app.get("/latest", async (req, res) => { + const objects = await s3.send( + new ListObjectsV2Command({ + Bucket: Resource.MyBucket.name, + }), + ); + + const latestFile = objects.Contents.sort( + (a, b) => b.LastModified - a.LastModified, + )[0]; + + const command = new GetObjectCommand({ + Key: latestFile.Key, + Bucket: Resource.MyBucket.name, + }); + const url = await getSignedUrl(s3, command); + + res.redirect(url); +}); + +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); diff --git a/examples/aws-express-file-upload/package.json b/examples/aws-express-file-upload/package.json new file mode 100644 index 000000000..9cf78d523 --- /dev/null +++ b/examples/aws-express-file-upload/package.json @@ -0,0 +1,20 @@ +{ + "name": "aws-express", + "version": "1.0.0", + "description": "", + "main": "index.js", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-s3": "^3.556.0", + "@aws-sdk/lib-storage": "^3.556.0", + "@aws-sdk/s3-request-presigner": "^3.556.0", + "express": "^4.19.2", + "multer": "^1.4.5-lts.1", + "sst": "latest" + }, + "devDependencies": { + "@types/aws-lambda": "8.10.137" + } +} diff --git a/examples/aws-express-file-upload/sst-env.d.ts b/examples/aws-express-file-upload/sst-env.d.ts new file mode 100644 index 000000000..99616cc2b --- /dev/null +++ b/examples/aws-express-file-upload/sst-env.d.ts @@ -0,0 +1,21 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "MyBucket": { + "name": string + "type": "sst.aws.Bucket" + } + "MyService": { + "service": string + "type": "sst.aws.Service" + "url": string + } + "MyVpc": { + "type": "sst.aws.Vpc" + } + } +} diff --git a/examples/aws-express-file-upload/sst.config.ts b/examples/aws-express-file-upload/sst.config.ts new file mode 100644 index 000000000..003429d60 --- /dev/null +++ b/examples/aws-express-file-upload/sst.config.ts @@ -0,0 +1,68 @@ +/// + +/** + * ## AWS Express file upload + * + * Deploys an Express app to AWS. + * + * You can get started by running. + * + * ```bash + * mkdir aws-express && cd aws-express + * npm init -y + * npm install express + * npx sst@latest init + * ``` + * + * Now you can add a service. + * + * ```ts title="sst.config.ts" + * cluster.addService("MyService", { + * public: { + * ports: [{ listen: "80/http", forward: "3000/http" }], + * }, + * dev: { + * command: "node --watch index.mjs", + * }, + * }); + * ``` + * + * Start your app locally. + * + * ```bash + * npx sst dev + * ``` + * + * This example lets you upload a file to S3 and then download it. + * + * ```bash + * curl --F file=@elysia.png http://localhost:3000/ + * curl http://localhost:3000/latest + * ``` + * + * Finally, you can deploy it using `npx sst deploy --stage production`. + */ +export default $config({ + app(input) { + return { + name: "aws-express", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const bucket = new sst.aws.Bucket("MyBucket"); + const vpc = new sst.aws.Vpc("MyVpc"); + + const cluster = new sst.aws.Cluster("MyCluster", { vpc }); + cluster.addService("MyService", { + public: { + ports: [{ listen: "80/http" }], + }, + dev: { + command: "node --watch index.mjs", + }, + link: [bucket], + }); + }, +}); diff --git a/examples/aws-express-file-upload/tsconfig.json b/examples/aws-express-file-upload/tsconfig.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/examples/aws-express-file-upload/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/examples/aws-express/.dockerignore b/examples/aws-express/.dockerignore index 3c3629e64..ea0aaeeec 100644 --- a/examples/aws-express/.dockerignore +++ b/examples/aws-express/.dockerignore @@ -1 +1,5 @@ node_modules + + +# sst +.sst \ No newline at end of file diff --git a/examples/aws-express/index.mjs b/examples/aws-express/index.mjs index 71564dd77..f810c756c 100644 --- a/examples/aws-express/index.mjs +++ b/examples/aws-express/index.mjs @@ -1,60 +1,26 @@ -import multer from "multer"; import express from "express"; import { Resource } from "sst"; -import { Upload } from "@aws-sdk/lib-storage"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { - S3Client, - GetObjectCommand, - ListObjectsV2Command, -} from "@aws-sdk/client-s3"; +import { Cluster } from "ioredis"; const PORT = 80; const app = express(); -const s3 = new S3Client({}); -const upload = multer({ storage: multer.memoryStorage() }); -app.get("/", async (req, res) => { - res.send("Hello World!"); -}); - -app.post("/", upload.single("file"), async (req, res) => { - const file = req.file; - const params = { - Bucket: Resource.MyBucket.name, - Key: file.originalname, - Body: file.buffer, - }; +const redis = new Cluster( + [{ host: Resource.MyRedis.host, port: Resource.MyRedis.port }], + { + dnsLookup: (address, callback) => callback(null, address), + redisOptions: { + tls: true, + username: Resource.MyRedis.username, + password: Resource.MyRedis.password, + }, + } +); - const upload = new Upload({ - params, - client: s3, - }); - - await upload.done(); - - res.status(200).send("File uploaded successfully."); -}); - -app.get("/latest", async (req, res) => { - const objects = await s3.send( - new ListObjectsV2Command({ - Bucket: Resource.MyBucket.name, - }), - ); - - const latestFile = objects.Contents.sort( - (a, b) => b.LastModified - a.LastModified, - )[0]; - - const command = new GetObjectCommand({ - Key: latestFile.Key, - Bucket: Resource.MyBucket.name, - }); - const url = await getSignedUrl(s3, command); - - res.redirect(url); +app.get("/", async (req, res) => { + const counter = await redis.incr("counter"); + res.send(`Hit counter: ${counter}`); }); app.listen(PORT, () => { diff --git a/examples/aws-express/package.json b/examples/aws-express/package.json index 9cf78d523..989b12331 100644 --- a/examples/aws-express/package.json +++ b/examples/aws-express/package.json @@ -3,18 +3,18 @@ "version": "1.0.0", "description": "", "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "@aws-sdk/client-s3": "^3.556.0", - "@aws-sdk/lib-storage": "^3.556.0", - "@aws-sdk/s3-request-presigner": "^3.556.0", - "express": "^4.19.2", - "multer": "^1.4.5-lts.1", - "sst": "latest" + "express": "^4.21.0", + "ioredis": "^5.4.1", + "sst": "3.1.65" }, "devDependencies": { - "@types/aws-lambda": "8.10.137" + "@types/aws-lambda": "8.10.145" } } diff --git a/examples/aws-express/sst-env.d.ts b/examples/aws-express/sst-env.d.ts index 99616cc2b..261f14f5b 100644 --- a/examples/aws-express/sst-env.d.ts +++ b/examples/aws-express/sst-env.d.ts @@ -5,9 +5,12 @@ import "sst" export {} declare module "sst" { export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" + "MyRedis": { + "host": string + "password": string + "port": number + "type": "sst.aws.Redis" + "username": string } "MyService": { "service": string @@ -15,6 +18,7 @@ declare module "sst" { "url": string } "MyVpc": { + "bastion": string "type": "sst.aws.Vpc" } } diff --git a/examples/aws-express/sst.config.ts b/examples/aws-express/sst.config.ts index f0a5436a9..ec2f80a28 100644 --- a/examples/aws-express/sst.config.ts +++ b/examples/aws-express/sst.config.ts @@ -9,18 +9,18 @@ export default $config({ }; }, async run() { - const bucket = new sst.aws.Bucket("MyBucket"); - const vpc = new sst.aws.Vpc("MyVpc"); - + const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); + const redis = new sst.aws.Redis("MyRedis", { vpc }); const cluster = new sst.aws.Cluster("MyCluster", { vpc }); + cluster.addService("MyService", { + link: [redis], public: { ports: [{ listen: "80/http" }], }, dev: { command: "node --watch index.mjs", }, - link: [bucket], }); - }, + } }); diff --git a/www/src/content/docs/docs/examples.mdx b/www/src/content/docs/docs/examples.mdx index 35f1e729d..4c22e587f 100644 --- a/www/src/content/docs/docs/examples.mdx +++ b/www/src/content/docs/docs/examples.mdx @@ -251,7 +251,7 @@ curl --F file=@elysia.png http://localhost:3000/ curl http://localhost:3000/latest ``` -Finally, you can deploy it using `bun sst deploy`. +Finally, you can deploy it using `bun sst deploy --stage production`. ```ts title="sst.config.ts" const bucket = new sst.aws.Bucket("MyBucket"); const vpc = new sst.aws.Vpc("MyVpc"); @@ -343,6 +343,66 @@ return { View the [full example](https://github.com/sst/ion/tree/dev/examples/aws-dynamo). +--- +## AWS Express file upload + +Deploys an Express app to AWS. + +You can get started by running. + +```bash +mkdir aws-express && cd aws-express +npm init -y +npm install express +npx sst@latest init +``` + +Now you can add a service. + +```ts title="sst.config.ts" +cluster.addService("MyService", { + public: { + ports: [{ listen: "80/http", forward: "3000/http" }], + }, + dev: { + command: "node --watch index.mjs", + }, +}); +``` + +Start your app locally. + +```bash +npx sst dev +``` + +This example lets you upload a file to S3 and then download it. + +```bash +curl --F file=@elysia.png http://localhost:3000/ +curl http://localhost:3000/latest +``` + +Finally, you can deploy it using `npx sst deploy --stage production`. +```ts title="sst.config.ts" +const bucket = new sst.aws.Bucket("MyBucket"); +const vpc = new sst.aws.Vpc("MyVpc"); + +const cluster = new sst.aws.Cluster("MyCluster", { vpc }); +cluster.addService("MyService", { + public: { + ports: [{ listen: "80/http" }], + }, + dev: { + command: "node --watch index.mjs", + }, + link: [bucket], +}); +``` + +View the [full example](https://github.com/sst/ion/tree/dev/examples/aws-express-file-upload). + + --- ## FFmpeg in Lambda diff --git a/www/src/content/docs/docs/start/aws/express.mdx b/www/src/content/docs/docs/start/aws/express.mdx index b368a3bf1..beff405d2 100644 --- a/www/src/content/docs/docs/start/aws/express.mdx +++ b/www/src/content/docs/docs/start/aws/express.mdx @@ -3,7 +3,7 @@ title: Express on AWS with SST description: Create and deploy an Express app to AWS with SST. --- -We are going to build an Express app in a container, add an S3 Bucket for file uploads, and deploy it to AWS using SST. +We are going to build a hit counter using Express and Redis. We'll the deploy it to AWS in a container using SST. :::tip[View source] You can [view the source](https://github.com/sst/ion/tree/dev/examples/aws-express) of this example in our repo. @@ -66,7 +66,7 @@ To deploy our Express app, let's add an [AWS Fargate](https://aws.amazon.com/far ```js title="sst.config.ts" {9-11} async run() { - const vpc = new sst.aws.Vpc("MyVpc"); + const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); const cluster = new sst.aws.Cluster("MyCluster", { vpc }); cluster.addService("MyService", { @@ -80,121 +80,100 @@ async run() { } ``` -This creates a VPC, uses it for a new ECS Cluster, adds a Fargate service to it, and exposes it through _http_. +This creates a VPC with a bastion host, an ECS Cluster, and adds a Fargate service to it. The `dev.command` tells SST to run our Express app locally in dev mode. --- -## 3. Add an S3 Bucket +## 3. Add Redis -Let's add an S3 Bucket for file uploads. Update your `sst.config.ts`. +Let's add an [Amazon ElastiCache](https://aws.amazon.com/elasticache/) Redis cluster. Add this below the `Vpc` component in your `sst.config.ts`. ```js title="sst.config.ts" -const bucket = new sst.aws.Bucket("MyBucket"); +const redis = new sst.aws.Redis("MyRedis", { vpc }); ``` -Add this above the `Vpc` component. +This shares the same VPC as our ECS cluster. --- -#### Link the bucket +#### Link Redis -Now, link the bucket to the container. +Now, link the Redis cluster to the container. ```ts title="sst.config.ts" {3} cluster.addService("MyService", { // ... - link: [bucket], + link: [redis], }); ``` -This will allow us to reference the bucket in our Express app. +This will allow us to reference the Redis cluster in our Express app. --- -#### Start dev mode +#### Install a tunnel -Start your app in dev mode. +Since our Redis cluster is in a VPC, we'll need a tunnel to connect to it from our local machine. -```bash -npx sst dev +```bash "sudo" +sudo npx sst tunnel install ``` -Once started, click on **MyService** in the sidebar for your local Express app. +This needs _sudo_ to create a network interface on your machine. --- -## 4. Upload a file +#### Start dev mode -We want the `/` route of our API to upload a file to our S3 Bucket. Let's start by installing the npm packages we'll use for the upload. +Start your app in dev mode. ```bash -npm install multer @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner +npx sst dev ``` -Add the relevant imports. +This will deploy your app, start a tunnel in the **Tunnel** tab, and run your Express app locally in the **MyServiceDev** tab. -```ts title="index.mjs" -import multer from "multer"; -import { Resource } from "sst"; -import { Upload } from "@aws-sdk/lib-storage"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { S3Client, GetObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3"; - -const s3 = new S3Client({}); -const upload = multer({ storage: multer.memoryStorage() }); -``` +--- -And add the route to your `index.mjs`. +## 4. Connect to Redis -```ts title="index.mjs" {4} -app.post("/", upload.single("file"), async (req, res) => { - const file = req.file; - const params = { - Bucket: Resource.MyBucket.name, - Key: file.originalname, - Body: file.buffer - }; +We want the `/` route of our API to increment a counter in our Redis cluster. Let's start by installing the npm packages we'll use. - const upload = new Upload({ - params, - client: s3, - }); +```bash +npm install ioredis +``` - await upload.done(); +Add the relevant imports to your `index.mjs`. - res.status(200).send("File uploaded successfully."); -}); +```ts title="index.mjs" {5} +import { Resource } from "sst"; +import { Cluster } from "ioredis"; + +const redis = new Cluster( + [{ host: Resource.MyRedis.host, port: Resource.MyRedis.port }], + { + dnsLookup: (address, callback) => callback(null, address), + redisOptions: { + tls: true, + username: Resource.MyRedis.username, + password: Resource.MyRedis.password, + }, + } +); ``` :::tip -We are directly accessing our S3 bucket with `Resource.MyBucket.name`. +We are directly accessing our Redis cluster with `Resource.MyRedis.*`. ::: ---- - -## 5. Download a file - -We want the `/latest` route of our app to generate a pre-signed URL to download the last uploaded file in our S3 Bucket. Add this to your `index.mjs`. +Let's update the `/` route. ```ts title="index.mjs" -app.get("/latest", async (req, res) => { - const objects = await s3.send( - new ListObjectsV2Command({ - Bucket: Resource.MyBucket.name, - }) - ); - - const latestFile = objects.Contents.sort((a, b) => b.LastModified - a.LastModified)[0]; - - const command = new GetObjectCommand({ - Key: latestFile.Key, - Bucket: Resource.MyBucket.name, - }); - const url = await getSignedUrl(s3, command); - - res.redirect(url); +app.get("/", async (req, res) => { + const counter = await redis.incr("counter"); + res.send(`Hit counter: ${counter}`); }); ``` @@ -202,17 +181,13 @@ app.get("/latest", async (req, res) => { #### Test your app -Let's try uploading a file from your project root. - -```bash -curl -F file=@package.json http://localhost:80 -``` +Let's head over to `http://localhost:80` in your browser and it'll show the current hit counter. -Now head over to `http://localhost:80/latest` in your browser and it'll download the file you just uploaded. +You should see it increment every time you refresh the page. --- -## 6. Deploy your app +## 5. Deploy your app To deploy our app we'll first add a `Dockerfile`.