Nếu bạn đã từng mệt mỏi với việc phải quản lý server, lo lắng về scaling, hay đơn giản là muốn tập trung 100% vào việc viết code logic, thì bài viết này là dành cho bạn. Hôm nay, chúng ta sẽ cùng nhau thực hiện một bước đi đầu tiên vào thế giới "Serverless" (phi máy chủ) đầy thú vị của Amazon Web Services (AWS) qua một dịch vụ cốt lõi: AWS Lambda.
Chúng ta sẽ cùng nhau xây dựng một function "Hello, World!" kinh điển, nhưng theo phong cách Lambda.
AWS Lambda là gì và tại sao bạn nên quan tâm?
Hiểu một cách đơn giản, AWS Lambda là một dịch vụ tính toán cho phép bạn chạy code mà không cần phải quản lý bất kỳ server nào.
Bạn chỉ cần upload code của mình lên, và Lambda sẽ lo toàn bộ phần còn lại: từ việc khởi tạo tài nguyên, thực thi code, scaling tự động (từ vài request đến hàng ngàn request mỗi giây), cho đến việc ghi log.
Những lợi ích chính khiến Lambda trở nên hấp dẫn:
- Không quản lý server (No Server Management): Quên đi việc vá lỗi hệ điều hành, cấu hình web server, hay theo dõi CPU/RAM. Bạn chỉ cần tập trung vào code.
- Chi phí hiệu quả (Pay-per-use): Bạn chỉ trả tiền cho thời gian code của bạn thực sự chạy, tính bằng mili giây. Khi không có request nào, bạn không tốn một đồng nào. Đây là một sự thay đổi lớn so với việc phải trả tiền cho một server chạy 24/7. Nghe có vẻ ngon nhưng hãy tính toán cẩn thận không là bạn sẽ nhận một quả bill to đùng!
- Tự động co giãn (Automatic Scaling): Lambda tự động nhân bản function của bạn để xử lý đồng thời nhiều request. Bạn không cần phải cấu hình Auto Scaling Group phức tạp.
- Tích hợp chặt chẽ với hệ sinh thái AWS: Lambda là "chất keo" kết dính các dịch vụ AWS lại với nhau. Bạn có thể trigger một Lambda function khi có file mới được upload lên S3, có bản ghi mới trong DynamoDB, hoặc qua một HTTP request từ API Gateway.
Viết code cho "Hello Lambda"
Sau khi function được tạo, bạn sẽ thấy một trình soạn thảo code ngay trên giao diện. AWS đã tạo sẵn cho chúng ta một đoạn code mẫu trong file index.mjs (hoặc index.js).
Code mẫu sẽ trông tương tự như thế này:
export const handler = async (event) => {
// TODO implement
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
Hãy cùng phân tích một chút:
- export const handler = async (event) => { ... }: Đây là điểm vào (entry point) của function. Khi Lambda được trigger, nó sẽ gọi hàm này. Việc sử dụng async/await là một chuẩn phổ biến và rất tiện lợi.
- event: Là một đối tượng (object) chứa dữ liệu đầu vào được gửi đến function. Ví dụ, nếu trigger từ API Gateway, event sẽ chứa thông tin về HTTP request (headers, body, path...).
- context (tham số thứ hai, không dùng trong ví dụ này): Chứa thông tin về môi trường thực thi (runtime information) như request ID, thời gian còn lại trước khi timeout...
Bây giờ, hãy "cá nhân hóa" thông điệp của chúng ta và thêm một dòng log để debug. Sửa lại code của bạn như sau:
export const handler = async (event) => {
// In ra event nhận được để debug (có thể xem trong CloudWatch Logs)console.log("Received event:", JSON.stringify(event, null, 2));
const message = "Xin chào từ AWS Lambda! Tôi là một lập trình viên Việt Nam viết bằng JavaScript.";
// Cấu trúc response này thường dùng khi tích hợp với API Gatewayconst response = {
statusCode: 200,
body: JSON.stringify(message), // JSON.stringify sẽ xử lý tốt tiếng Việt
};
return response;
};
"Hello Lambda" chỉ là cánh cửa mở ra một thế giới rộng lớn. Từ đây, bạn có thể khám phá:
- Tích hợp với API Gateway: Tạo một endpoint HTTP để có thể gọi Lambda function của bạn từ trình duyệt hoặc bất kỳ client nào. Đây là cách phổ biến để xây dựng các API backend serverless.
// Sử dụng AWS SDK v3 - cách làm hiện đại
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
// Khởi tạo DynamoDB client. Nên đặt ngoài handler để tái sử dụng.
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.TABLE_NAME; // Lấy tên bảng từ biến môi trường
export const handler = async (event) => {
console.log("Event: ", JSON.stringify(event, null, 2));
const method = event.httpMethod;
const path = event.path;
try {
if (method === "POST" && path === "/products") {
// Parse body từ request
const product = JSON.parse(event.body);
// Thêm ID và ngày tạo
product.id = Date.now().toString();
product.createdAt = new Date().toISOString();
// Tạo command để thêm item vào DynamoDB
const command = new PutCommand({
TableName: TABLE_NAME,
Item: product,
});
await docClient.send(command);
return {
statusCode: 201, // 201 Created
headers: { "Content-Type": "application/json" },
body: JSON.stringify(product),
};
}
if (method === "GET" && event.pathParameters && event.pathParameters.id) {
const productId = event.pathParameters.id;
// Tạo command để lấy item
const command = new GetCommand({
TableName: TABLE_NAME,
Key: {
id: productId,
},
});
const { Item } = await docClient.send(command);
if (!Item) {
return {
statusCode: 404,
body: JSON.stringify({ error: "Product not found" }),
};
}
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(Item),
};
}
// Trường hợp không khớp method/path
return {
statusCode: 400,
body: JSON.stringify({ error: "Invalid request" }),
};
} catch (error) {
console.error("Error:", error);
return {
statusCode: 500,
body: JSON.stringify({ error: "Internal Server Error" }),
};
}
};
- Trigger từ S3: Cấu hình để Lambda tự động chạy mỗi khi có một file ảnh mới được upload lên S3, sau đó bạn có thể dùng code để resize ảnh đó (sử dụng thư viện như sharp).
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import sharp from 'sharp';
const s3Client = new S3Client({});
const DEST_BUCKET = process.env.DEST_BUCKET;
const THUMB_WIDTH = 200; // Chiều rộng của thumbnail
export const handler = async (event) => {
// S3 trigger gửi event trong một mảng Records
const record = event.Records[0];
const srcBucket = record.s3.bucket.name;
// Key là tên file, cần decode ký tự đặc biệt như dấu cách (+)
const srcKey = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
// Ngăn chặn vòng lặp vô tận: Nếu file đã là thumbnail thì không xử lý nữa
if (srcKey.endsWith('-thumb.jpg')) {
console.log("This is already a thumbnail. Skipping.");
return;
}
try {
// 1. Lấy ảnh gốc từ S3 source
const getObjectParams = {
Bucket: srcBucket,
Key: srcKey,
};
const getCommand = new GetObjectCommand(getObjectParams);
const originalImage = await s3Client.send(getCommand);
// Chuyển Body (dạng stream) sang buffer để sharp xử lý
const streamToBuffer = (stream) =>
new Promise((resolve, reject) => {
const chunks = [];
stream.on("data", (chunk) => chunks.push(chunk));
stream.on("error", reject);
stream.on("end", () => resolve(Buffer.concat(chunks)));
});
const imageBuffer = await streamToBuffer(originalImage.Body);
// 2. Resize ảnh bằng sharp
console.log(`Resizing image: ${srcKey}`);
const resizedBuffer = await sharp(imageBuffer)
.resize({ width: THUMB_WIDTH })
.toFormat('jpeg') // Chuyển sang định dạng jpeg cho thumbnail
.toBuffer();
// 3. Upload ảnh đã resize vào S3 destination
const destKey = srcKey.replace(/(\.[\w\d_-]+)$/i, '-thumb.jpg');
const putObjectParams = {
Bucket: DEST_BUCKET,
Key: destKey,
Body: resizedBuffer,
ContentType: 'image/jpeg',
};
const putCommand = new PutObjectCommand(putObjectParams);
await s3Client.send(putCommand);
console.log(`Successfully resized ${srcBucket}/${srcKey} and uploaded to ${DEST_BUCKET}/${destKey}`);
return { status: "OK" };
} catch (error) {
console.error("Error processing image:", error);
throw error;
}
};
- Quản lý code với Infrastructure as Code (IaC): Sử dụng các công cụ như AWS SAM (Serverless Application Model) hoặc Serverless Framework để định nghĩa Lambda function và các tài nguyên liên quan bằng code, giúp việc quản lý và deploy trở nên chuyên nghiệp hơn.