Xây dựng Serverless API với AWS API Gateway, Lambda (Nodejs), MongoDB Atlas
Bối cảnh
Cấp Cao Chiên Da được chủ tạch Tập đoàn Toàn Đập Đá giao nhiệm vụ trong một tuần phải xây dựng xong hệ thống quản lý thông tin thiết bị để phục vụ công tác chào mừng đại lễ kỷ niệm sắp tới.
Hệ thống khá phức tạp với nhiều thành phần công nghệ khác nhau, trong đó có một module cần một API nho nhỏ để gọi lấy thông tin là chính, việc cập nhật thông tin cũng cần nhưng không cần tức thì, chủ yếu dùng cho việc thống kê vào cuối ngày là chính.
GET, OPTIONS api.tapdoantoandapda.com/device-detail/?id=xxx
POST api.tapdoantoandapda.com/device-detail
GET api.tapdoantoandapda.com/device-pinout/?id=xxx
Với nhu cầu đó Chiên Da quyết định sẽ sử dụng Mongo Altlas gói miễn phí, vì cũng không có mấy endpoint và tính năng đơn giản nên sẽ sử dụng công nghệ Lambda serverless cộng với API Gateway (API GTW) để expose API này ra ngoài Internet. Ở ngay chỗ API GTW đã có tính năng cache và custom domain “có vẻ” đủ xài rồi nhưng với tiêu chí phục vụ chủ tịch là nhiệm vụ của mình, còn trả tiền bill cloud thì là của chủ tịch nên Chiên Da đã quyết định bổ sung thêm CloudFront là lớp cuối cùng để giao tiếp với end-user, qua đó có thể dễ dàng control cache cũng như là sử dụng alias domain được quản lý ở Route 53.
Nhân những ngày uể oải đợi nghỉ lễ sắp tới, à không, nhân những ngày sục sôi không khí nhà nhà thi đua số lượng line of code, người người thi đua tìm và bắt bug nên tôi đã dành chút thời gian để viết lại quá trình xây dựng hệ thống này trước là để sau này lỡ có phải nhìn lại code của chính bản thân thì cũng bớt chút cảm giác lạ lẫm sau là để cho quý anh chị em nào có cần thiết thì hãy sử dụng. Tôi rất lấy làm hân hạnh vì đều này, mong rằng sẽ hữu ích và giúp quý anh chị em có thêm được thời gian để sống cuộc đời bên ngoài màn hình. (Đây là đoạn tự sự của Chiên Da, tác giả bài viết -LTS).
MongoDB Atlas
Ở thời điểm viết bài này MongoDB cho phép sử dụng Shared Cluster với dung lượng lưu trữ 512MB ở trên cả 3 cloud provider AWS/ Azure/ GCP. Bạn có thể xem chi tiết tại https://www.mongodb.com/pricing.
Lambda Function
File app.mjs của mình sẽ như bên dưới
// Filename: app.mjs
'use strict';
console.log('init the function 15');
import * as mongoose from 'mongoose';
// START: Schema
/**
* Mongoose schema for the 'test' collection.
* @typedef {Object} TestSchema
* @property {string} name - The name of the test.
*/
const testSchema = new mongoose.Schema({ name: String });
// END: Schema
/**
* Handles the incoming request and returns the appropriate response based on the event and context.
*
* @param {Object} event - The event object containing information about the request.
* @param {Object} context - The context object containing information about the runtime environment.
* @return {Promise<Object>} The response object with a statusCode and body property.
*/
async function handleRequest(event, context) {
const { httpMethod, path } = event;
if ((httpMethod === "GET" || httpMethod === "OPTIONS") && path.indexOf("/device-detail") >= 0) {
return await getDeviceDetail(event, context);
} else if (httpMethod === "POST" && path.indexOf("/device-detail") >=0) {
return await postDeviceDetail(event, context);
} else if ((httpMethod === "GET" || httpMethod === "OPTIONS") && path.indexOf("/device-pinout") >= 0) {
return await getDevicePinout(event, context);
} else {
return {
statusCode: 404,
body: "Not found",
};
}
}
/**
* Handles the incoming event and context, and returns the result of handleRequest.
*
* @param {Object} event - The event object.
* @param {Object} context - The context object.
* @return {Promise} A promise that resolves to the result of handleRequest.
*/
export const handler = async (event, context) => {
try {
return await handleRequest(event, context);
} catch (error) {
console.error("Error adding new link:", error);
return {
statusCode: 500,
body: JSON.stringify({
error: error.message,
message: 'fetch_error',
data: null,
}),
};
}
};
File hỗ trợ kết nối MongoDB Atlas
// File: mongoConnectionHelper.mjs
// https://mongoosejs.com/docs/lambda.html
import mongoose from 'mongoose';
let conn = null;
const uri = process.env.MONGODB_URI;
export async function connect() {
if (conn == null) {
conn = mongoose.createConnection(uri, {
serverSelectionTimeoutMS: 5000,
useNewUrlParser: true,
useUnifiedTopology: true,
});
// `await`ing connection after assigning to the `conn` variable
// to avoid multiple function calls creating new connections
await conn.asPromise();
}
return conn;
}
Vì đặc trưng của Lambda function thì sẽ thực thi trong hàm handler nên mình sẽ xây dựng thêm hàm handleRequest
để xử lý các endpoint nhận được theo đúng các method. Với 3 method ở trên, mình thử nghiệm chạy tốn khoảng 105MB memory.
Trong source code, mình có sử dụng package mongosee nên cần phải chạy npm install [package-name] trên máy local rồi sau đó zip luôn cả folder node_modules để upload lên Lambda. Không rõ ngay đây có cách nào thao tác hay hơn không.
Thật ra thì nếu chỉ cần làm MVP hay demo cho sếp coi thì tới đây cũng là đủ rồi vì Lambda cho phép expose một HTTPS URL (Enable function URL) với đầy đủ các tính năng cơ bản vốn có của một API như:
- Auth type: sử dụng AWS_IAM hoặc public
- Invoke mode:
- Configure cross-origin resource sharing (CORS): có thể tự cấu hình lại các origin, expose headers, allow headers, allow methods, max age.
Tuy nhiên vì hạn chế lớn nhất của Function URL là mỗi URL chỉ trỏ được đến đúng 1 version hoặc alias thôi, không thể thay đổi được giá trị trỏ đến nên sẽ rất khó để quản lý và sử dụng thực tế, ở môi trường cần sự ổn định ít thay đổi các giá trị cấu hình.
API Gateway (API GTW)
API GTW sẽ giải quyết câu chuyện Function URL ở trên, cũng tại màn hình Function overview của Lambda, ta bấm Add trigger để tiến hành chọn API GTW từ trong source của Trigger configuration.
Làm vầy thì tiện nhưng cái API url tạo ra đều bị đính kèm thêm một đoạn tên của Lambda Function, khá bất tiện thế nên chúng ta sẽ chọn giải pháp tự tạo API GTW riêng.
Từ màn hình API GTW Console bấm nút Create API > Choose an API type > REST API > Build. Ở màn hình tiếp theo ta chỉ cần điền API name, còn lại mọi thứ để như mặc định.
Chọn Actions > Create Resource để tạo một nhóm resource, nhóm này sẽ chứa các endpoint. Chỗ này thì nhớ chọn Enable API Gateway CORS, với tùy chọn này thì sau khi tạo xong sẽ mặc định có endpoint method OPTIONS phục vụ cho việc preflight (một phương pháp để kiểm tra xem browser có quyền truy cập vào tài nguyên đích không).
Tiếp tục chọn Actions > Create Method, chọn method click nút check kế bên sau đó điền Lambda Function, nhớ chọn Use Lambda Proxy integration để request package gửi lên được nhét vào event
theo đúng cấu trúc code ở Lambda ở trên.
Sau khi xong lại chọn Actions > Deploy API, hoàn tất bạn sẽ thấy màn hình production Stage Editor với Invoke URL.
Đến đây xem như cơ bản đã hoàn thành và có thể đưa vào sử dụng.
Anh Dũng
Sài Gòn những ngày nhiều mưa, tháng 09/ 2023.
All rights reserved