Serverless Typescript với AWS Lambda, API Gateway và DynamoDB trên môi trường offline - Phần 02
Bài đăng này đã không được cập nhật trong 6 năm
Như ở bài viết trước chúng ta đã tạo được một ứng dụng serverless có thể deploy lên hệ sinh thái aws, thực hiện phát triển ở mội trường local.
Với cấu trúc code như đã viết trong bài trước, mỗi một chức năng sẽ được xử lý bởi một hàm và được trỏ bởi một api endpoind.
functions:
createCat:
handler: handler.createCat
events:
- http:
path: cats
method: POST
findCatById:
handler: handler.findCatById
events:
- http:
path: cats/:id
method: GET
Cấu trúc code theo hướng này được gọi là Microservices Pattern theo serverless framework. Kiểu này có một nhược điểm lớn: Giới hạn kích thước của CloudFormation template (khoảng 200 resources), ứng dụng của chúng ta sẽ có giới hạn về số lượng các chức năng.
Ví dụ: Một chức năng createCat
chúng ta sẽ cần ít nhất 4 resource: 1 AWS::Lambda::Function + 1 AWS::Lambda::Permission(ApiGateway) + 1 AWS::Lambda::Version + 1 AWS::Logs::LogGroup
Mỗi bảng dynamodb chúng ta mất một tài nguyên(dynamodb chỉ định nghĩa bảng không định nghĩa database (??))
Mình sẽ viết một bài viết khác để chia sẻ về các kiểu cấu trúc code
cho một dự án theo hướng serverless.
Serverless có vẻ còn khá mới, để phát triển một ứng dụng với thời gian nhanh nhất có thể hoặc tất cả các members trong team có thể tham gia phát triền với mô hình serverless mà không cần quá nhiều thời gian tìm hiểu, chúng ta sẽ sử dụng những cấu trúc cũ
để giải quyết vấn đề. Monolithic Pattern một function giải quyết tất cả tính năng (khoc2) (tất nhiên hướng này cũng có những nhược điểm nhất định).
Do mục đích cuối cùng là xây dựng một ứng dụng cung cấp API chuẩn RESTFul chúng ta sẽ nghĩ tới việc sử dụng express framework để tạo ra một ứng dụng như vậy.
Việc chuyển đổi từ một trigger event của Aws Lambda Function sang request theo express các bạn có thể đọc bài viết này https://viblo.asia/p/expressjs-style-flow-for-aws-lambda-vyDZOXa9lwj của tác giải @Kinyakin (bow).
Còn trong bài viết này chúng ta sẽ sử dụng package serverless-http
để làm việc đó (yaoming).
Xây dựng ứng dụng với Express bằng Typescript
Cấu trúc thư mục
Chúng ta sẽ vẫn dùng thư mục của bài viết trước. Chúng ta sẽ chia thư mục giống thế này.
src
----modules // các thư mục tính năng theo đối tượng
--------cats // code của module `cats`
------------dto // các Data Transfer Objects của cats module
------------interfaces // các Interface của cats module
------------repositories // các repo của cats module
------------cats.controller.ts // file chứa nội dung router của cats module
----shared // các thư viện dùng chung cho cả app
----App.ts // class mô tả ứng dụng express
----handler.ts // file chứa hàm handler của serverless (từ bài trước)
----localServer.ts // file để thực hiện chạy ứng dụng theo cách `thông thường`, không qua `serverless-offline`
index.js // hỗ trợ chạy ứng dụng theo cách thông thường mà không cần build typescript
Những file khác vẫn giữa như bài viết trước.
Hiện thực các file chức năng
Mình sẽ không nêu các packages sẽ phải thêm, chúng ta thấy cần thư viện nào thì sẽ thêm thư viện đó (khoc2)
Main app và helper
src/App.js
file này export ra một instance ứng dụng express. Class App mô tả một ứng dụng express,
constructor
sẽ khởi tạo và gọi các hàm định nghĩa của ứng dụng- Phương thức
private configureApp
cấu hình ứng dụng - Phương thức
private mountRoutes
thực hiện mapping url với controller
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as logger from 'morgan';
import CatsController from './modules/cats/cats.controller';
import {Application, Request, Response, NextFunction, Router} from "express";
interface ErrorRequest {
status?: number;
message?: string;
}
class App {
private express: Application;
constructor() {
this.express = express();
this.configureApp();
this.mountRoutes();
}
public getExpress(): Application {
return this.express;
}
private configureApp(): void {
this.express.use(logger('dev'));
this.express.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader("Access-Control-Allow-Origin", "*");
});
this.express.use(bodyParser.json());
this.express.use(bodyParser.urlencoded({extended: false}));
}
private mountRoutes(): void {
const route = Router();
route.get('/', (rep: Request, res: Response, next: NextFunction) => {
res.json({
message: 'Hello World!',
});
});
this.express.use('/', route);
//configure api routes
this.express.use('/api/v1/cats', CatsController);
// 404
this.express.use((req: Request, res: Response, next: NextFunction) => {
res.status(404).json({message: 'Not found'});
});
// 5xx
this.express.use((err: ErrorRequest, req: Request, res: Response, next: NextFunction) => {
res.status(err.status || 500).json({message: err.message || 'Internal Server Error'});
});
}
}
export default new App().getExpress();
src/shared/dynamoDbConnection.ts
thực hiện việc kết nối và thư thi trên cơ sở dữ liệu Dynamodb
constructor
thực hiện việc định nghĩa cấu hình kết nối, tạo ra đối tượng document clientpublic create
thêm mới dữ liệu vào 1 bảngpublic findById
tìm một đối trượng trong 1 bảng theo id
import {DocumentClient} from "aws-sdk/lib/dynamodb/document_client";
class Connection {
private dynamoDb: DocumentClient;
constructor() {
if (process.env.IS_OFFLINE === 'true') {
this.dynamoDb = new DocumentClient({
endpoint: process.env.DYNAMODB_ENDPOINT || 'http://localhost:8000',
region: 'localhost',
});
} else {
this.dynamoDb = new DocumentClient();
}
}
public create<T>(params: DocumentClient.PutItemInput): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.dynamoDb.put(params, (err, data) => {
if (err) {
return reject(err);
}
resolve(params.Item as T);
});
});
}
public findById<T>(params: DocumentClient.GetItemInput): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.dynamoDb.get(params, (err, data) => {
if (err) {
return reject(err);
}
resolve(data.Item as T || null);
});
});
}
}
export default new Connection();
Cats module
Các file trong phần này sẽ được tạo trong thư mục src/modules/cats
dto/create-cat.dto.ts
mô tả class dữ liệu để tạo mới một con mèo, sử dụng package class-validator
để định nghĩa và validate dữ liệu.
import {IsDefined, MaxLength, MinLength} from 'class-validator';
class CreateCatDto {
@MinLength(3, {
message: 'name is too short',
})
@MaxLength(50, {
message: 'name is to long',
})
public name: string;
@IsDefined({
message: 'mood should be defined',
})
public mood: string;
constructor(name: string, mood: string) {
this.name = name;
this.mood = mood;
}
}
export default CreateCatDto;
interfaces/cat.interface.ts
Mô tả interface một đối tượng m èo
interface ICat {
id: string;
name: string;
mood: string;
}
export default ICat;
repositories/cats.repo.ts
mô tả class CatsRepo
- làm việc với bảng cats
public create
tạo mới một con mèopublic findById
tìm một con mèo theo Id
import { validate } from 'class-validator';
import * as uuid from 'uuid';
import CreateCatDto from '../dto/create-cat.dto';
import ICat from '../interfaces/cat.interface';
import connection from '../../../shared/dynamoDbConnection';
class CatsRepo {
private tableName: string;
constructor() {
this.tableName = process.env.CATS_TABLE || 'cats'
}
public async create(name: string, mood: string): Promise<ICat> {
const catDto = new CreateCatDto(name, mood);
const errors = await validate(catDto, { validationError: { target: false } });
if (errors.length > 0) {
return Promise.reject({ status: 400, message: 'Cat data is not valid!' });
}
const params = {
Item: {
id: uuid.v4(),
mood: catDto.mood,
name: catDto.name,
},
TableName: this.tableName,
};
return connection.create<ICat>(params);
}
public async findById(id: string): Promise<ICat> {
const params = {
Key: {
id,
},
TableName: this.tableName,
};
let cat = await connection.findById<ICat>(params);
if (!cat) {
return Promise.reject({ status: 404, message: 'No cat found with the given id.' });
}
return cat;
}
}
export default new CatsRepo();
cats.controller.ts
mô tả class CatsController
- mapping url với phương thức tương ứng
constructor
khởi tạo đối tượngroute
và mapping các phương thứcpublic create
handle phương thức POST /api/v1/cats tạo mới một con mèopublic findById
handle phương thức GET /api/v1/cats/:id lấy một con mèo theo idprivate init
mapping
import {NextFunction, Request, Response, Router} from 'express';
import CatsRepo from './repositories/cats.repo';
class CatsController {
private route: Router;
constructor() {
this.route = Router();
this.init();
}
public getRoute(): Router {
return this.route;
}
public async create(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { name, mood } = req.body;
const cat = await CatsRepo.create(name, mood);
res.status(200).json({
data: cat,
message: 'success',
status: res.status,
});
} catch (err) {
next(err);
}
}
public async findById(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const id = req.params.id;
const cat = await CatsRepo.findById(id);
res.status(200).json({
data: cat,
message: 'success',
status: res.status,
});
} catch (err) {
next(err);
}
}
private init(): void {
this.route
.post('/', this.create)
.get('/:id', this.findById);
}
}
export default new CatsController().getRoute();
Cấu hình ứng dụng chạy local
Vì được viết bằng express framework nên chúng ta có thể thực hiện chạy và test như cách thông thường
mà không cần sự hỗ trợ của package serverless-offline
.
Chúng ta đã có một đối tượng App
là một instance của express app, chúng ta sẽ tiến hành khởi tạo http server theo express instance này
src/localServer.ts
require('dotenv').config();
import * as http from 'http';
import App from './App';
const port = normalizePort(process.env.PORT || 3000);
App.set('port', port);
const server = http.createServer(App);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
function normalizePort(val: number | string): number | string | boolean {
let port: number = (typeof val === 'string') ? parseInt(val, 10) : val;
if (isNaN(port)) {
return val;
} else if (port >= 0) {
return port;
} else {
return false;
}
}
function onError(error: NodeJS.ErrnoException): void {
if (error.syscall !== 'listen') { throw error; }
const bind = (typeof port === 'string') ? 'Pipe ' + port : 'Port ' + port;
switch (error.code) {
case 'EACCES':
console.error(`${bind} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
console.error(`${bind} is already in use`);
process.exit(1);
break;
default:
throw error;
}
}
function onListening(): void {
const addr = server.address();
const bind = (typeof addr === 'string') ? `pipe ${addr}` : `port ${addr.port}`;
console.log(`Listening on ${bind}`);
}
Tới đây chúng ta đã có thể khởi chạy server bằng cách chạy các lệnh
tsc -p tsconfig.json
-- để compile ts file sang file js
node src/localServer.js
-- khởi động ứng dụng
Chúng ta sẽ có một http server chạy trên cổng process.env.PORT || 3000
Việc compile mất khá nhiều thời gian nên chúng ta sẽ dùng thêm một file index.js
ở thư mục root của project, sử dụng package ts-node/register
để tạo ra môi trường thực thi Typescript.
require('ts-node/register');
require('./src/localServer');
!!! Vì ứng dụng không chạy bằng serverless
, nên chúng ta phải cài đặt dynamodb trên máy local. Có thể dùng docker hoặc cài đặt theo hướng dẫn từ AWS https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html
Cấu hình serverless handle function với instance của express
Chúng ta sẽ sửa lại file src/handler.ts
, các trigger request của Lambda funtion sẽ được convert
sang http request (trigger function phải là API Gateway request) bằng package serverless-http
import serverless = require('serverless-http');
import App from './App';
module.exports.handler = serverless(App);
Và thay đổi nội dung file serverless.yml
, chỉ còn 1 function, toàn bộ các http request sẽ được xử lý bởi function này
service: serverless-typescript-express
provider:
name: aws
runtime: nodejs6.10
stage: dev
plugins:
- serverless-webpack
- serverless-dynamodb-local
- serverless-offline
custom:
dynamodb:
start:
port: 8000
inMemory: true
migrate: true
seed: true
# Uncomment only if you already have a DynamoDB running locally
noStart: true
functions:
app:
handler: handler.handler
events:
- http: ANY /
- http: 'ANY {proxy+}'
resources:
Resources:
usersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: cats
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
Như vậy là hoàn thành ứng dụng serverless theo express style
.
Kết luận
Bằng cấu trúc này chúng ta có thể chuyển một ứng dụng express cũ sang dạng serverless, hoặc phát triển một ứng dụng mới theo hướng serverless sử dụng hệ sinh thái Aws một cách dễ dàng.
Chúng ta sẽ gặp nhau ở các bài tiếp theo xoay quanh chủ đề serverless
.
All rights reserved