├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── docs └── serverless.md ├── package-lock.json ├── package.json ├── readme.md ├── renovate.json ├── serverless.yml ├── src ├── handlers │ ├── hello-async │ │ ├── __test__ │ │ │ ├── index_test.ts │ │ │ └── mock_event.json │ │ └── index.ts │ ├── hello-kinesis-stream │ │ ├── __test__ │ │ │ ├── index_test.ts │ │ │ └── mock_event.json │ │ └── index.ts │ └── index.ts ├── helpers │ └── kinesis-stream-helper.ts ├── index.ts └── interfaces │ ├── base.ts │ ├── kinesis-stream-event.d.ts │ └── lambda-proxy.d.ts ├── tsconfig.json ├── tsconfig.test.json └── tslint.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## one sentence summary of changes 2 | 3 | #### Is it a breaking change?: NO / YES 4 | 5 | 6 | ### **Why** did you make these changes? 7 | 8 | ex) I need to access `X` filed from api 9 | 10 | 11 | ### **What** did you changed? 12 | 13 | ex) I added `X` field to `Y` entity. 14 | 15 | 16 | ### What do you especially want to get reviewed? 17 | 18 | The method of rendering `X` field 19 | 20 | ### Is there any other comments that every teammate should know? 21 | 22 | You should use `X` field instead of using `Y` field 23 | 24 | 25 | ### Submission Type 26 | 27 | * [ ] Bugfix 28 | * [ ] New Feature 29 | * [ ] Refactor 30 | 31 | 32 | ### Checklist 33 | 34 | * [ ] test for core logics? 35 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | job: 5 | runs-on: ubuntu-latest 6 | container: node:12 7 | timeout-minutes: 15 8 | services: 9 | mysql: 10 | image: mysql:5.7 11 | env: 12 | MYSQL_ROOT_PASSWORD: root-password 13 | MYSQL_DATABASE: vingle 14 | MYSQL_USER: vingle 15 | MYSQL_PASSWORD: vingle 16 | options: >- 17 | --health-cmd "mysqladmin ping -h 127.0.0.1" --health-interval 10s --health-timeout 5s --health-retries 5 18 | postgres: 19 | image: postgres:9.4 20 | env: 21 | POSTGRES_PASSWORD: vingle 22 | options: >- 23 | --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 24 | dynamodb: 25 | image: amazon/dynamodb-local 26 | # @note Github overwrites WORKDIR to repository path, so overwrite that again 27 | options: >- 28 | --workdir /home/dynamodblocal 29 | --health-cmd "curl --fail http://127.0.0.1:8000/shell/ || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5 30 | redis: 31 | image: redis 32 | options: >- 33 | --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 34 | steps: 35 | - uses: actions/checkout@v1 36 | - name: Setup 37 | run: | 38 | apt-get update 39 | apt-get install -y zip 40 | - name: Prepare 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | run: | 44 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc 45 | npm ci 46 | - name: Lint 47 | run: npm run lint 48 | - name: Test 49 | run: npm test 50 | env: 51 | MYSQL_URL: mysql://vingle:vingle@mysql:3306/vingle 52 | POSTGRES_URL: postgres://postgres:vingle@postgres:5432/postgres 53 | DYNAMO_TYPES_ENDPOINT: http://dynamodb:8000 54 | REDIS_URL: redis://redis:6379/10 55 | # - name: Deploy 56 | # if: github.event_name == 'push' && github.ref == 'refs/heads/master' 57 | # run: npm run deploy:prod 58 | # env: 59 | # AWS_DEFAULT_REGION: us-east-1 60 | # AWS_REGION: us-east-1 61 | # AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 62 | # AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dst 2 | dst.zip 3 | 4 | .env 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | 44 | .serverless 45 | 46 | # IDE Configurations 47 | .vscode 48 | .idea 49 | -------------------------------------------------------------------------------- /docs/serverless.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balmbees/lambda-microservice-template/d6e536b2b819a334d1921936903428fbd7f3ce29/docs/serverless.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-microservice-template", 3 | "version": "1.0.1", 4 | "description": "Template for microservice using lambda / typescript / serverless", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/balmbees/lambda-microservice-template.git" 9 | }, 10 | "scripts": { 11 | "prebuild": "check-engine && rm -rf dst", 12 | "build": "tsc", 13 | "postbuild": "cd src && find . -name '*.json' -type f -exec cp {} ../dst/{} \\; && cd ..", 14 | "prepack": "rm -f dst.zip", 15 | "pack": "cp package.json package-lock.json dst/ && cd dst && npm ci --production && npm ls && zip -rqy ../dst.zip . && cd ..", 16 | "pretest": "npm run build -- -p ./tsconfig.test.json", 17 | "test": "mocha --exit -t 20000 dst/**/__test__/**/*.js", 18 | "lint": "tslint -c tslint.json 'src/**/*.ts'", 19 | "deploy": "npm run build && npm run pack && serverless deploy", 20 | "deploy:stage": "npm run deploy -- -s stage", 21 | "deploy:prod": "npm run deploy -- -s prod", 22 | "info:stage": "sls info -s stage", 23 | "info:prod": "sls info -s prod" 24 | }, 25 | "engines": { 26 | "node": "^12.0.0", 27 | "npm": "^6.10.2" 28 | }, 29 | "author": "Kurt Lee", 30 | "license": "ISC", 31 | "devDependencies": { 32 | "@types/chai": "4.2.16", 33 | "@types/mocha": "5.2.7", 34 | "@types/node": "8.10.59", 35 | "@vingle/serverless-tag-plugin": "1.1.3", 36 | "@vingle/tslint-preset": "1.0.1", 37 | "chai": "4.3.4", 38 | "check-engine": "1.10.0", 39 | "mocha": "6.2.3", 40 | "serverless": "1.83.3", 41 | "serverless-prune-plugin": "1.5.0", 42 | "tslint": "6.1.3", 43 | "typescript": "3.9.6" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Microservice Name 2 | 3 | ## Bussiness Domain 4 | Simple, Easy to read, "One Sentence" Description 5 | 6 | ``` 7 | ex) User Recommendation On Vingle 8 | Feed on Vingle 9 | Interest Recommendation On Vingle 10 | ``` 11 | 12 | ## Responsibility 13 | Clear and Specific description about Responsibility of the service. 14 | be mind that it should specify the "work" that could be directly understandable to developer also. such as "Rendering" / "Business Logic" / "Persistency" / "Tracking".. etc 15 | 16 | ``` 17 | ex) user recommendation business logic & recommendation history logging 18 | ``` 19 | 20 | 21 | ## Architecture Diagram 22 | Simple diagram representing the architecture, which showing 23 | 1. Infrastrcture resources the service is using. such as "DynamoDB" / "RDS" 24 | 2. Other microservices that the service is consuming. such as "Vingle Feed" / "Color Extractor" 25 |

26 | 27 | ## Infrastructures 28 | Link to resources that deployed. such as link to "Cloudformation" or "Lambda" or "CloudWatch" 29 | 30 | 31 | ## Usage 32 | 33 | Description about "How developer consume this service" 34 | 35 | Most commonly it will be "HTTP Restful API", so than you could just list up the APIs. 36 | 37 | ``` 38 | Content-Type: 'application/json' 39 | Authorization: 'token' 40 | 41 | GET api/users/recommendations 42 | POST api/users/recommendations/:recommendationId/feedback 43 | ``` 44 | 45 | but also, it could be a simple background worker such as Kinesis Stream consumer, or S3 Event handler 46 | 47 | in such case, just describe how we can "Use" this service by sending event on Kinesis Stream or S3 48 | 49 | 50 | ## Maintainer 51 | 52 | Specify who is mainly responsible for this service. meaning of responsible here is that who knows this service most, and who can help and manage other peoples to make change on the service. 53 | it could be serveral peoples also 54 | 55 | ``` 56 | ex) spam-checker -> mooyeol 57 | TrackTicket -> Kurt / Shin 58 | ``` 59 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@vingle", 4 | "@vingle:nodejs12", 5 | "@vingle:lambda" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: microservice-template 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | stage: ${opt:stage, 'stage'} 7 | logRetentionInDays: 30 8 | 9 | plugins: 10 | - serverless-prune-plugin 11 | - "@vingle/serverless-tag-plugin" 12 | 13 | custom: 14 | prune: 15 | automatic: true 16 | number: 5 17 | 18 | package: 19 | artifact: dst.zip 20 | 21 | functions: 22 | helloAsync: 23 | handler: handlers.helloAsync 24 | events: 25 | - http: 26 | path: hello 27 | method: get 28 | -------------------------------------------------------------------------------- /src/handlers/hello-async/__test__/index_test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import handler from "../index"; 4 | const mockEvent = require("./mock_event.json"); // tslint:disable-line 5 | 6 | describe("HelloAsync", () => { 7 | describe("handler", () => { 8 | it("should work", async () => { 9 | const res = await handler(mockEvent); 10 | expect(res.statusCode).to.be.eq(200); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/handlers/hello-async/__test__/mock_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "some resource", 3 | "path": "path", 4 | "httpMethod": "GET", 5 | "headers": { 6 | "Accept": "application/json", 7 | "Accept-Encoding": "utf-8", 8 | "Accept-Language": "en", 9 | "CloudFront-Forwarded-Proto": "", 10 | "CloudFront-Is-Desktop-Viewer": "", 11 | "CloudFront-Is-Mobile-Viewer": "", 12 | "CloudFront-Is-SmartTV-Viewer": "", 13 | "CloudFront-Is-Tablet-Viewer": "", 14 | "CloudFront-Viewer-Country": "", 15 | "Host": "www.lvh.me:3000", 16 | "Upgrade-Insecure-Requests": "", 17 | "User-Agent": "WEBKIT/SAFARI", 18 | "Via": "Web", 19 | "X-Amz-Cf-Id": "", 20 | "X-Forwarded-For": "", 21 | "X-Forwarded-Port": "", 22 | "X-Forwarded-Proto": "", 23 | "original-uri": "www.lvh.me:3000/hello" 24 | }, 25 | "queryStringParameters": { 26 | "param_1": "PARAM DATA", 27 | "param_2": "PARAM DATA" 28 | }, 29 | "body": "SOME EVENT BODY" 30 | } 31 | -------------------------------------------------------------------------------- /src/handlers/hello-async/index.ts: -------------------------------------------------------------------------------- 1 | import * as LambdaProxy from "../../interfaces/lambda-proxy"; 2 | 3 | export default async function handler(event: LambdaProxy.Event) { 4 | const response = await new Promise((resolve, reject) => { 5 | setTimeout(() => { 6 | resolve({ 7 | statusCode: 200, 8 | headers: { 9 | "Content-Type": "text/html", 10 | }, 11 | body: ` 12 | 13 | 14 |

TITLE

15 | 16 | 17 | `, 18 | }); 19 | }, 1); 20 | }); 21 | 22 | return response; 23 | } 24 | -------------------------------------------------------------------------------- /src/handlers/hello-kinesis-stream/__test__/index_test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | chai.should(); 3 | 4 | import handler from "../index"; 5 | const mockEvent = require("./mock_event.json"); // tslint:disable-line 6 | 7 | describe("HelloKinesisStream", () => { 8 | describe("handler", () => { 9 | it("should work", async () => { 10 | const result = await handler(mockEvent); 11 | result.should.be.deep.eq({ 12 | count: 1, 13 | payloads: [ 14 | { 15 | sampleData: [ 16 | 100, 17 | 200, 18 | ], 19 | }, 20 | ], 21 | }); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/handlers/hello-kinesis-stream/__test__/mock_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [{ 3 | "kinesis": { 4 | "data": "eyJzYW1wbGVEYXRhIjpbMTAwLDIwMF19" 5 | } 6 | }] 7 | } 8 | -------------------------------------------------------------------------------- /src/handlers/hello-kinesis-stream/index.ts: -------------------------------------------------------------------------------- 1 | import KinesisStreamHelper from "../../helpers/kinesis-stream-helper"; 2 | import * as KinesisStreamEvent from "../../interfaces/kinesis-stream-event"; 3 | 4 | export default async function handler(event: KinesisStreamEvent.Event) { 5 | const payloads = (event.Records || []).map((record) => { 6 | return KinesisStreamHelper.parseRecordData(record); 7 | }); 8 | 9 | return { 10 | count: payloads.length, 11 | payloads, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/handlers/index.ts: -------------------------------------------------------------------------------- 1 | 2 | // List of handlers 3 | import helloAsync from "./hello-async"; 4 | 5 | const handlers = { 6 | helloAsync, 7 | }; 8 | 9 | export = handlers; 10 | -------------------------------------------------------------------------------- /src/helpers/kinesis-stream-helper.ts: -------------------------------------------------------------------------------- 1 | import * as KinesisStreamEvent from "../interfaces/kinesis-stream-event"; 2 | 3 | export default class KinesisStreamHelper { 4 | public static parseRecordData(record: KinesisStreamEvent.KinesisRecord) { 5 | const payload = new Buffer(record.kinesis.data, "base64").toString("utf8"); 6 | const payloadJson = JSON.parse(payload); 7 | return payloadJson; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balmbees/lambda-microservice-template/d6e536b2b819a334d1921936903428fbd7f3ce29/src/index.ts -------------------------------------------------------------------------------- /src/interfaces/base.ts: -------------------------------------------------------------------------------- 1 | // Event 2 | export interface Event {} // tslint:disable-line 3 | export interface Response {} // tslint:disable-line 4 | 5 | // Context 6 | 7 | // Type definitions for AWS Lambda 8 | // Definitions by: James Darbyshire (http://darb.io) 9 | 10 | /************************************************ 11 | * * 12 | * AWS Lambda API * 13 | * * 14 | ************************************************/ 15 | 16 | // Context 17 | // http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html 18 | export interface Context { 19 | // Properties 20 | functionName: string; 21 | functionVersion: string; 22 | invokedFunctionArn: string; 23 | memoryLimitInMB: number; 24 | awsRequestId: string; 25 | logGroupName: string; 26 | logStreamName: string; 27 | identity?: CognitoIdentity; 28 | clientContext?: ClientContext; 29 | getRemainingTimeInMillis(): number; 30 | } 31 | 32 | interface CognitoIdentity { 33 | cognito_identity_id: string; 34 | cognito_identity_pool_id: string; 35 | } 36 | 37 | interface ClientContext { 38 | client: ClientContextClient; 39 | Custom?: any; 40 | env: ClientContextEnv; 41 | } 42 | 43 | interface ClientContextClient { 44 | installation_id: string; 45 | app_title: string; 46 | app_version_name: string; 47 | app_version_code: string; 48 | app_package_name: string; 49 | } 50 | 51 | interface ClientContextEnv { 52 | platform_version: string; 53 | platform: string; 54 | make: string; 55 | model: string; 56 | locale: string; 57 | } 58 | -------------------------------------------------------------------------------- /src/interfaces/kinesis-stream-event.d.ts: -------------------------------------------------------------------------------- 1 | import * as Base from "./base"; 2 | 3 | // Event 4 | export interface Event extends Base.Event { 5 | Records?: KinesisRecord[]; 6 | } 7 | 8 | export interface KinesisRecord { 9 | kinesis: { 10 | partitionKey: string; 11 | kinesisSchemaVersion: string; 12 | data: string; 13 | sequenceNumber: string; 14 | }; 15 | eventSource: string; 16 | eventID: string; 17 | invokeIdentityArn: string; 18 | eventVersion: string; 19 | eventName: string; 20 | eventSourceARN: string; 21 | awsRegion: string; 22 | } 23 | 24 | // Response 25 | export interface Response extends Base.Response { 26 | [key: string]: any; 27 | } 28 | 29 | // Context 30 | export type Context = Base.Context; 31 | -------------------------------------------------------------------------------- /src/interfaces/lambda-proxy.d.ts: -------------------------------------------------------------------------------- 1 | import * as Base from "./base"; 2 | 3 | // Event 4 | export interface Event extends Base.Event { 5 | resource?: string; 6 | path?: string; 7 | httpMethod?: string; 8 | headers: EventHeaders; 9 | queryStringParameters: EventQueryStringParameters; 10 | pathParameters?: EventPathParameters; 11 | stageVariables?: EventStageVariables; 12 | requestContext?: { 13 | "accountId": string; 14 | "resourceId": string; 15 | "stage": string; 16 | "requestId": string; 17 | "identity": { 18 | "cognitoIdentityPoolId": any; 19 | "accountId": any; 20 | "cognitoIdentityId": any; 21 | "caller": any; 22 | "apiKey": any; 23 | "sourceIp": string, 24 | "accessKey": any; 25 | "cognitoAuthenticationType": any; 26 | "cognitoAuthenticationProvider": any; 27 | "userArn": any; 28 | "userAgent": string; 29 | "user": any; 30 | } 31 | "resourcePath": string; 32 | "httpMethod": string; 33 | "apiId": string; 34 | }; 35 | body?: string; 36 | } 37 | 38 | export interface EventHeaders { 39 | "Accept"?: string; 40 | "Accept-Encoding"?: string; 41 | "Accept-Language"?: string; 42 | "CloudFront-Forwarded-Proto"?: string; 43 | "CloudFront-Is-Desktop-Viewer"?: string; 44 | "CloudFront-Is-Mobile-Viewer"?: string; 45 | "CloudFront-Is-SmartTV-Viewer"?: string; 46 | "CloudFront-Is-Tablet-Viewer"?: string; 47 | "CloudFront-Viewer-Country"?: string; 48 | "Host"?: string; 49 | "Upgrade-Insecure-Requests"?: string; 50 | "User-Agent"?: string; 51 | "Via"?: string; 52 | "X-Amz-Cf-Id"?: string; 53 | "X-Forwarded-For"?: string; 54 | "X-Forwarded-Port"?: string; 55 | "X-Forwarded-Proto"?: string; 56 | "original-uri"?: string; 57 | } 58 | 59 | export interface EventQueryStringParameters { 60 | [key: string]: string; 61 | } 62 | 63 | export interface EventPathParameters { 64 | [key: string]: string; 65 | } 66 | 67 | export interface EventStageVariables { 68 | [key: string]: string; 69 | } 70 | 71 | // Response 72 | export interface Response extends Base.Response { 73 | statusCode: number; 74 | headers: { 75 | "Content-Type": string, 76 | }; 77 | body: string; 78 | } 79 | 80 | export type Context = Base.Context; 81 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "noImplicitAny": true, 6 | "sourceMap": true, 7 | "outDir": "dst", 8 | "strict": false, 9 | "strictNullChecks": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "lib": [ 13 | "es2017" 14 | ] 15 | }, 16 | "exclude": [ 17 | "**/__test__/" 18 | ], 19 | "include": [ 20 | "src/**/*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "noImplicitAny": true, 6 | "sourceMap": true, 7 | "outDir": "dst", 8 | "strict": false, 9 | "strictNullChecks": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "lib": [ 13 | "es2017" 14 | ] 15 | }, 16 | "include": [ 17 | "src/**/*" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@vingle/tslint-preset"] 3 | } 4 | --------------------------------------------------------------------------------