diff --git a/aws-node-typescript-multi-instance-lambda/.eslintrc.js b/aws-node-typescript-multi-instance-lambda/.eslintrc.js new file mode 100644 index 000000000..36123a34d --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + ecmaVersion: 2018, + sourceType: "module", + }, + env: { + "jest/globals": true, + es6: true, + }, + plugins: [ + "jest", + "sonarjs", + "@typescript-eslint", + "prettier", + ], +}; diff --git a/aws-node-typescript-multi-instance-lambda/.gitignore b/aws-node-typescript-multi-instance-lambda/.gitignore new file mode 100644 index 000000000..02ffabe9e --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/.gitignore @@ -0,0 +1,105 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ +.webpack/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/aws-node-typescript-multi-instance-lambda/.images/aws-lambda-screenshot.png b/aws-node-typescript-multi-instance-lambda/.images/aws-lambda-screenshot.png new file mode 100644 index 000000000..a90b471ff Binary files /dev/null and b/aws-node-typescript-multi-instance-lambda/.images/aws-lambda-screenshot.png differ diff --git a/aws-node-typescript-multi-instance-lambda/.nvmrc b/aws-node-typescript-multi-instance-lambda/.nvmrc new file mode 100644 index 000000000..25bf17fc5 --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/.nvmrc @@ -0,0 +1 @@ +18 \ No newline at end of file diff --git a/aws-node-typescript-multi-instance-lambda/LICENSE b/aws-node-typescript-multi-instance-lambda/LICENSE new file mode 100644 index 000000000..026c439aa --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Daniel Simpson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/aws-node-typescript-multi-instance-lambda/README.md b/aws-node-typescript-multi-instance-lambda/README.md new file mode 100644 index 000000000..dbd522d5c --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/README.md @@ -0,0 +1,78 @@ +# Multi-instance Serverless AWS Lambda function example + +This example demonstrates how to deploy multiple instances (or copies) of the same lambda function by using the "--param" `sls` option to pass in an instance name. The main idea is to rename AWS resources and outputs which would otherwise clash using the instance name. + +A lambda instance's configuration can then be made instance-specific by adding the instance name to its environment variable or parameter store keys. + +The benefit of this approach is that only one lambda function needs to be defined in `serverless.ts` (or `serverless.yml`), but any number of copies of it can be deployed directly from the command line. + +## Setup + +1. Install Node.js 18 (recommended: [Node Version Manager](https://github.com/nvm-sh/nvm#install--update-script)). + +2. Install packages: + ```bash + npm i + ``` + +## Use case + +When you want to deploy multiple lambda functions with the same implementation, but you don't want to explicitly define each copy in `serverless.yml`. + +For example, you could deploy the lambda copies in a continuous deployment (CD) pipeline and configure them to fetch data from different sources based on their instance names. + +## Usage + +### Deployment + +To deploy an instance "foo" of the lambda `hello` to the default `dev` stage in the `ap-southeast-2` region , run: + +```bash +npx sls deploy --region ap-southeast-2 --param="instance=foo" +``` + +Another instance, "bar", can be deployed with: + +```bash +npx sls deploy --region ap-southeast-2 --param="instance=bar" +``` + +Now, two separate yet identical `hello` lambdas are available on AWS: +Both instances of `hello` are deployed. +
+ +Note that the "--param" option is now required when running other `serverless` commands, such as `sls info`: + +```bash +npx sls info --region ap-southeast-2 --param="instance=foo" +``` + +### Invocation + +To invoke an instance of `hello` using `sls invoke`, pass the instance name to the "--param" option: + +```bash +npx sls invoke -f hello --region ap-southeast-2 --param="instance=foo" +``` + +Output: + +``` +{ + "statusCode": 200, + "body": "{\n \"message\": \"Function `aws-node-typescript-multi-instance-lambda-foo-dev-hello` executed successfully.\",\n \"input\": {}\n}" +} +``` + +### Removal + +"--param" is also required to run the `sls remove` command to tear down the CloudFormation stack associated with a specific instance. + +```bash +npx sls remove --region ap-southeast-2 --param="instance=foo" +``` + +## Acknowledgements + +- [This comment](https://github.com/serverless/serverless/issues/9361#issuecomment-884602588) by [**@rdemorais**](https://github.com/rdemorais) on Serverless issue [#9361](https://github.com/serverless/serverless/issues/9361). +- [**@billkidwell**](https://github.com/billkidwell)'s [Simple Kinesis Example](https://github.com/serverless/examples/tree/v3/aws-node-typescript-kinesis). diff --git a/aws-node-typescript-multi-instance-lambda/handler.ts b/aws-node-typescript-multi-instance-lambda/handler.ts new file mode 100644 index 000000000..6f902e515 --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/handler.ts @@ -0,0 +1,13 @@ +import { Context } from "aws-lambda"; + +module.exports.hello = async (event: unknown, context: Context) => ({ + statusCode: 200, + body: JSON.stringify( + { + message: `Function \`${context.functionName}\` executed successfully.`, + input: event, + }, + null, + 2 + ), +}); diff --git a/aws-node-typescript-multi-instance-lambda/package.json b/aws-node-typescript-multi-instance-lambda/package.json new file mode 100644 index 000000000..1591f1a07 --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/package.json @@ -0,0 +1,33 @@ +{ + "name": "aws-node-typescript-multi-instance-lambda", + "version": "1.0.0", + "description": "Serverless example demonstrating how to deploy multiple instances of the same lambda", + "main": "serverless.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Daniel Simpson", + "license": "MIT", + "devDependencies": { + "@serverless/typescript": "^3.25.0", + "@types/aws-lambda": "^8.10.109", + "@types/node": "^18.11.11", + "@typescript-eslint/eslint-plugin": "^5.45.1", + "@typescript-eslint/parser": "^5.45.1", + "eslint": "^8.29.0", + "import": "^0.0.6", + "jest": "^29.3.1", + "prettier": "^2.8.1", + "serverless": "^3.25.1", + "serverless-webpack": "^5.11.0", + "sonarjs": "^1.0.0", + "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.3", + "webpack": "^5.75.0" + }, + "dependencies": { + "aws-lambda": "^1.0.7", + "source-map-support": "^0.5.21" + } +} diff --git a/aws-node-typescript-multi-instance-lambda/serverless.ts b/aws-node-typescript-multi-instance-lambda/serverless.ts new file mode 100644 index 000000000..d8b3a46f9 --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/serverless.ts @@ -0,0 +1,105 @@ +import type { AWS } from "@serverless/typescript"; + +const serverlessConfiguration: AWS = { + service: "aws-node-typescript-multi-instance-lambda", + frameworkVersion: "3", + configValidationMode: "error", + plugins: ["serverless-webpack"], + provider: { + name: "aws", + runtime: "nodejs18.x", + // region: "ap-southeast-2", + // Rename CloudFormation stack to include the instance name + stackName: "${self:service}-${param:instance}-${sls:stage}", + }, + functions: { + hello: { + // Rename function to include the instance name + name: "${self:service}-${param:instance}-${sls:stage}-hello", + handler: "handler.hello", + environment: { + INSTANCE_NAME: "${param:instance}", + }, + }, + }, + + resources: { + Resources: { + IamRoleLambdaExecution: { + Type: "AWS::IAM::Role", + Properties: { + Policies: [ + { + PolicyName: { + "Fn::Join": [ + "-", + [ + "${self:service}", + "${param:instance}", + "${sls:stage}", + "lambda", + ], + ], + }, + }, + ], + // Include instance name to avoid resource collision between instances + RoleName: { + "Fn::Join": [ + "-", + [ + // "${self:service}", + "multi-instance", // shorten service name due to 64-character "roleName" length requirement + "${param:instance}", + "${sls:stage}", + { + Ref: "AWS::Region", + }, + "lambdaRole", + ], + ], + }, + }, + }, + }, + Outputs: { + // Rename output export names to include instance name + // https://github.com/serverless/serverless/issues/9361#issuecomment-884602588 + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html + HelloLambdaFunctionQualifiedArn: { + Export: { + Name: { + "Fn::Join": [ + "-", + [ + "sls", + "${self:service}", + "${param:instance}", + "${sls:stage}", + "HelloLambdaFunctionQualifiedArn", + ], + ], + }, + }, + }, + ServerlessDeploymentBucketName: { + Export: { + Name: { + "Fn::Join": [ + "-", + [ + "sls", + "${self:service}", + "${param:instance}", + "${sls:stage}", + "ServerlessDeploymentBucketName", + ], + ], + }, + }, + }, + }, + }, +}; + +module.exports = serverlessConfiguration; diff --git a/aws-node-typescript-multi-instance-lambda/tsconfig.json b/aws-node-typescript-multi-instance-lambda/tsconfig.json new file mode 100644 index 000000000..c2db12811 --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "outDir": "lib", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noErrorTruncation": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true + }, + "include": ["handler.ts", "serverless.ts", "webpack.config.js"], + "exclude": ["node_modules/**/*", ".serverless/**/*"] +} diff --git a/aws-node-typescript-multi-instance-lambda/webpack.config.js b/aws-node-typescript-multi-instance-lambda/webpack.config.js new file mode 100644 index 000000000..4fba40cd0 --- /dev/null +++ b/aws-node-typescript-multi-instance-lambda/webpack.config.js @@ -0,0 +1,23 @@ +const path = require('path'); +const slsw = require('serverless-webpack'); + +module.exports = { + mode: slsw.lib.webpack.isLocal ? 'development' : 'production', + entry: slsw.lib.entries, + devtool: 'source-map', + resolve: { + extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], + }, + output: { + libraryTarget: 'commonjs', + path: path.join(__dirname, '.webpack'), + filename: '[name].js', + }, + target: 'node', + module: { + rules: [ + // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` + { test: /\.tsx?$/, loader: 'ts-loader' }, + ], + }, +}; \ No newline at end of file