AWS Lambda CloudWatch Jobs TypeScript
Hướng dẫn toàn diện về thiết lập công việc định kỳ với AWS Lambda và CloudWatch/EventBridge sử dụng TypeScript - từ cơ bản đến thực hành tốt nhất
Cần tự động hóa các tác vụ định kỳ như sao lưu, gửi email, hoặc xử lý dữ liệu? AWS Lambda kết hợp với CloudWatch Events/EventBridge là giải pháp serverless hoàn hảo để thay thế các công việc cron truyền thống. Hướng dẫn này tập trung vào triển khai TypeScript/Node.js cho các công việc định kỳ sẵn sàng cho production.
Tổng quan về công việc định kỳ Lambda#
AWS Lambda là dịch vụ tính toán serverless cho phép bạn chạy mã mà không cần quản lý máy chủ. Khi kết hợp với các dịch vụ lập lịch, bạn có thể:
- Tự động hóa tác vụ: Sao lưu, tạo báo cáo, dọn dẹp dữ liệu
- Tiết kiệm chi phí: Chỉ trả tiền khi mã thực thi
- Tự động mở rộng: Xử lý tải tăng mà không cần cấu hình
- Tính sẵn sàng cao: AWS đảm bảo tính sẵn sàng
[!NOTE] AWS hiện đề xuất sử dụng EventBridge Scheduler thay vì CloudWatch Events truyền thống cho các công việc định kỳ mới.
flowchart LR
subgraph Scheduler["Trình kích hoạt (Trigger)"]
direction TB
EBS["EventBridge Scheduler<br/>(Hỗ trợ múi giờ / Retry / Flexible)"]
CWE["CloudWatch Events<br/>(Cron truyền thống)"]
end
subgraph Serverless["Xử lý Serverless"]
Lambda["AWS Lambda Function<br/>(Node.js / TypeScript)"]
DLQ["Dead Letter Queue (SQS)<br/>(Lưu trữ lỗi khi gọi thất bại)"]
end
subgraph Targets["Tác vụ & Dịch vụ liên quan"]
DB[(Cơ sở dữ liệu / S3)]
Log["CloudWatch Logs / Alarms"]
end
EBS -->|Invoke định kỳ| Lambda
CWE -.->|Invoke cơ bản| Lambda
Lambda -.->|Gặp sự cố / Retry hết| DLQ
Lambda -->|Đọc / Ghi dữ liệu| DB
Lambda -->|Ghi log giám sát| Log
EventBridge Scheduler là dịch vụ lập lịch hiện đại với các tính năng nâng cao so với CloudWatch Events.
Ưu điểm của EventBridge Scheduler#
- Hỗ trợ múi giờ: Không bị giới hạn chỉ UTC
- Cửa sổ thời gian linh hoạt: Chạy trong các phạm vi thời gian linh hoạt
- Tự động thử lại: Tự động thử lại khi thất bại
- Lịch trình một lần: Hỗ trợ thực thi đơn lẻ
- Dead Letter Queue: Xử lý lỗi tốt hơn
Bước 1: Tạo hàm Lambda TypeScript#
import { Context } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
import { logger } from './utils/logger';
interface ScheduledEvent {
time: string;
detail?: any;
}
interface LambdaResponse {
statusCode: number;
body: string;
}
export const lambdaHandler = async (
event: ScheduledEvent,
context: Context
): Promise<LambdaResponse> => {
logger.info(`Công việc được kích hoạt tại: ${new Date().toISOString()}`);
logger.info(`Sự kiện: ${JSON.stringify(event)}`);
try {
// Logic xử lý công việc của bạn
const result = await processScheduledTask(event);
return {
statusCode: 200,
body: JSON.stringify({
message: 'Công việc hoàn thành thành công',
result
})
};
} catch (error) {
logger.error(`Công việc thất bại: ${error}`);
throw error;
}
};
async function processScheduledTask(event: ScheduledEvent): Promise<any> {
// Ví dụ: dọn dẹp cơ sở dữ liệu, gửi báo cáo, v.v.
logger.info('Đang xử lý công việc định kỳ...');
// Logic kinh doanh của bạn ở đây
return { status: 'success', processedItems: 10 };
}typescriptBước 2: Tạo vai trò IAM#
Lambda cần quyền để thực thi công việc và ghi log:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}jsonQuyền chính sách:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:*",
"s3:*"
],
"Resource": "*"
}
]
}jsonBước 3: Tạo lịch trình với EventBridge Scheduler#
Sử dụng AWS Console#
- Đi đến EventBridge Scheduler console
- Nhấp vào Create schedule
- Chọn Recurring schedule
- Cấu hình biểu thức lịch trình:
# Chạy hàng ngày lúc 9:00 AM UTC
cron(0 9 * * ? *)
# Chạy mỗi 15 phút
rate(15 minutes)
# Chạy hàng tuần vào Thứ Hai 9:00 AM
cron(0 9 ? * MON *)
# Chạy vào ngày đầu tiên của mỗi tháng
cron(0 0 1 * ? *)bash- Chọn hàm Lambda làm mục tiêu
- Cấu hình múi giờ (nếu cần):
# 9:00 AM Eastern Time
cron(0 9 ? * MON *)
timezone: America/New_Yorkbash- Cấu hình cửa sổ thời gian linh hoạt (tùy chọn):
# Chạy trong vòng 15 phút sau thời gian định kỳ
mode: FLEXIBLE
maximum_window_in_minutes: 15bash- Thiết lập chính sách thử lại:
maximum_retry_attempts: 3
maximum_event_age_in_seconds: 3600bashSử dụng AWS CLI#
aws scheduler create-schedule \
--name daily-job \
--schedule-expression 'cron(0 9 * * ? *)' \
--schedule-expression-timezone 'UTC' \
--target '{
"Arn": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
"RoleArn": "arn:aws:iam::123456789012:role/scheduler-role"
}' \
--flexible-time-window '{
"Mode": "FLEXIBLE",
"MaximumWindowInMinutes": 15
}'bashSử dụng Terraform#
resource "aws_scheduler_schedule" "daily_job" {
name = "daily-job"
group_name = "default"
flexible_time_window {
mode = "FLEXIBLE"
maximum_window_in_minutes = 15
}
schedule_expression = "cron(0 9 * * ? *)"
schedule_expression_timezone = "UTC"
target {
arn = aws_lambda_function.my_function.arn
role_arn = aws_iam_role.scheduler_role.arn
retry_policy {
maximum_retry_attempts = 3
maximum_event_age_in_seconds = 3600
}
dead_letter_config {
arn = aws_sqs_queue.dlq.arn
}
}
}hclPhương pháp 2: CloudWatch Events (Truyền thống)#
CloudWatch Events là phương pháp truyền thống, vẫn được hỗ trợ nhưng có ít tính năng hơn.
Bước 1: Tạo hàm Lambda#
Giống như phương pháp Scheduler.
Bước 2: Tạo quy tắc CloudWatch#
Sử dụng AWS Console#
- Đi đến CloudWatch console
- Chọn Events → Rules
- Nhấp vào Create rule
- Chọn Schedule expression
- Nhập biểu thức cron:
# 9:00 AM UTC hàng ngày
0 9 * * ? *
# Mỗi 5 phút
rate(5 minutes)bash- Chọn hàm Lambda làm mục tiêu
- Cấu hình quyền (AWS sẽ tự động tạo)
Sử dụng AWS CLI#
aws events put-rule \
--name daily-lambda-rule \
--schedule-expression 'cron(0 9 * * ? *)'
aws events put-targets \
--rule daily-lambda-rule \
--targets '{
"Id": "1",
"Arn": "arn:aws:lambda:us-east-1:123456789012:function:my-function"
}'
aws lambda add-permission \
--function-name my-function \
--statement-id daily-lambda-rule \
--action 'lambda:InvokeFunction' \
--principal events.amazonaws.com \
--source-arn arn:aws:events:us-east-1:123456789012:rule/daily-lambda-rulebashHướng dẫn biểu thức Cron#
EventBridge sử dụng biểu thức cron 6 trường:
cron(Phút Giờ Ngày-trong-tháng Tháng Ngày-trong-tuần Năm)textVí dụ biểu thức Cron#
# Hàng ngày lúc 9:00 AM UTC
cron(0 9 * * ? *)
# Hàng tuần vào Thứ Hai 9:00 AM
cron(0 9 ? * MON *)
# Hàng tháng vào ngày 1 lúc 00:00
cron(0 0 1 * ? *)
# Mỗi 15 phút
rate(15 minutes)
# Mỗi 1 giờ
rate(1 hour)
# 9:00 AM từ Thứ Hai đến Thứ Sáu
cron(0 9 ? * MON-FRI *)
# Ngày cuối cùng của tháng
cron(0 0 L * ? *)
# Thứ Hai đầu tiên của mỗi tháng
cron(0 0 ? * MON#1 *)bashGiá trị trường#
| Trường | Giá trị | Ký tự đặc biệt |
|---|---|---|
| Phút | 0-59 | , - * / |
| Giờ | 0-23 | , - * / |
| Ngày trong tháng | 1-31, L, W | , - * / ? L W |
| Tháng | 1-12, JAN-DEC | , - * / |
| Ngày trong tuần | 1-7, SUN-SAT, ?, L, # | , - * / ? L # |
| Năm | 1970-2199 | , - * / |
[!WARNING] Sử dụng
?khi ngày-trong-tháng hoặc ngày-trong-tuần bị giới hạn, không sử dụng cả hai trường cùng lúc.
Thực hành tốt nhất#
1. Định danh (Idempotency)#
Đảm bảo handler của bạn có tính định danh để thử lại an toàn:
import { DynamoDB } from 'aws-sdk';
const dynamodb = new DynamoDB.DocumentClient();
const TABLE_NAME = 'job-locks';
export const lambdaHandler = async (event: ScheduledEvent): Promise<any> => {
const jobId = `${event.time}-${event.detail?.id || 'default'}`;
// Kiểm tra xem công việc đã chạy chưa
if (await isJobAlreadyProcessed(jobId)) {
logger.info('Công việc đã được xử lý, bỏ qua');
return { status: 'skipped' };
}
// Xử lý công việc
const result = await processJob(event);
// Đánh dấu công việc đã được xử lý
await markJobAsProcessed(jobId);
return result;
};
async function isJobAlreadyProcessed(jobId: string): Promise<boolean> {
try {
const result = await dynamodb.get({
TableName: TABLE_NAME,
Key: { jobId }
}).promise();
return !!result.Item;
} catch (error) {
logger.error(`Lỗi khi kiểm tra trạng thái công việc: ${error}`);
return false;
}
}
async function markJobAsProcessed(jobId: string): Promise<void> {
await dynamodb.put({
TableName: TABLE_NAME,
Item: {
jobId,
processedAt: new Date().toISOString(),
ttl: Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60) // TTL 7 ngày
}
}).promise();
}typescript2. Dead Letter Queue (DLQ)#
Thiết lập DLQ để xử lý các lượt gọi thất bại:
import { SQS } from 'aws-sdk';
const sqs = new SQS();
const DLQ_URL = process.env.DLQ_URL || '';
async function sendToDLQ(errorMessage: string, event: ScheduledEvent): Promise<void> {
await sqs.sendMessage({
QueueUrl: DLQ_URL,
MessageBody: JSON.stringify({
error: errorMessage,
event,
timestamp: new Date().toISOString()
})
}).promise();
}
export const lambdaHandler = async (event: ScheduledEvent): Promise<any> => {
try {
const result = await processJob(event);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Công việc thất bại: ${errorMessage}`);
// Gửi đến DLQ
await sendToDLQ(errorMessage, event);
throw error;
}
};typescript3. Ngăn chặn chồng chéo#
Ngăn chặn các lần thực thi chồng chéo với khóa DynamoDB:
import { DynamoDB } from 'aws-sdk';
const dynamodb = new DynamoDB.DocumentClient();
const LOCK_TABLE = 'job-locks';
interface LockItem {
jobId: string;
lockedAt: string;
expiresAt: string;
}
async function acquireLock(jobId: string): Promise<boolean> {
const now = new Date();
const expiresAt = new Date(now.getTime() + 10 * 60 * 1000); // 10 phút
try {
await dynamodb.put({
TableName: LOCK_TABLE,
Item: {
jobId,
lockedAt: now.toISOString(),
expiresAt: expiresAt.toISOString()
},
ConditionExpression: 'attribute_not_exists(jobId)'
}).promise();
return true;
} catch (error) {
if ((error as any).code === 'ConditionalCheckFailedException') {
return false; // Khóa đã tồn tại
}
throw error;
}
}
async function releaseLock(jobId: string): Promise<void> {
await dynamodb.delete({
TableName: LOCK_TABLE,
Key: { jobId }
}).promise();
}
export const lambdaHandler = async (event: ScheduledEvent): Promise<any> => {
const jobId = `scheduled-job-${event.time}`;
// Mở khóa
const lockAcquired = await acquireLock(jobId);
if (!lockAcquired) {
logger.info('Công việc đang chạy, bỏ qua');
return { status: 'skipped', reason: 'lock_not_acquired' };
}
try {
const result = await processJob(event);
return result;
} finally {
// Giải phóng khóa
await releaseLock(jobId);
}
};typescript4. Giám sát và ghi log#
Sử dụng CloudWatch Logs và Metrics:
import { CloudWatch } from 'aws-sdk';
const cloudwatch = new CloudWatch({ region: process.env.AWS_REGION });
export const lambdaHandler = async (event: ScheduledEvent, context: Context): Promise<any> => {
const startTime = Date.now();
try {
// Metrics tùy chỉnh
logger.info('JOB_STARTED');
const result = await processJob(event);
// Ghi log thời gian
const duration = Date.now() - startTime;
logger.info(`JOB_COMPLETED duration=${duration}ms`);
// Gửi metric tùy chỉnh
await cloudwatch.putMetricData({
Namespace: 'ScheduledJobs',
MetricData: [
{
MetricName: 'JobDuration',
Value: duration,
Unit: 'Milliseconds',
Dimensions: [
{
Name: 'JobName',
Value: 'DailyCleanup'
}
]
}
]
}).promise();
return result;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`JOB_FAILED error=${error} duration=${duration}ms`);
// Gửi metric thất bại
await cloudwatch.putMetricData({
Namespace: 'ScheduledJobs',
MetricData: [
{
MetricName: 'JobFailures',
Value: 1,
Unit: 'Count',
Dimensions: [
{
Name: 'JobName',
Value: 'DailyCleanup'
}
]
}
]
}).promise();
throw error;
}
};typescript5. Xử lý lỗi#
Xử lý lỗi một cách tinh tế:
interface APIResponse {
statusCode: number;
body: string;
}
export const lambdaHandler = async (event: ScheduledEvent): Promise<APIResponse> => {
try {
const result = await processJob(event);
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (error) {
if (error instanceof ValidationError) {
logger.error(`Lỗi xác thực: ${error.message}`);
return {
statusCode: 400,
body: JSON.stringify({ error: error.message })
};
} else if (error instanceof BusinessError) {
logger.error(`Lỗi kinh doanh: ${error.message}`);
return {
statusCode: 422,
body: JSON.stringify({ error: error.message })
};
} else {
logger.error(`Lỗi không mong đợi: ${error}`);
// Gửi đến DLQ hoặc cảnh báo
await sendAlert(error instanceof Error ? error.message : String(error));
throw error;
}
}
};
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
class BusinessError extends Error {
constructor(message: string) {
super(message);
this.name = 'BusinessError';
}
}typescriptSo sánh: EventBridge Scheduler vs CloudWatch Events#
| Tính năng | EventBridge Scheduler | CloudWatch Events |
|---|---|---|
| Loại lịch trình | cron + rate + one-time | cron + rate |
| Hỗ trợ múi giờ | ✅ Múi giờ bất kỳ | ❌ Chỉ UTC |
| Cửa sổ linh hoạt | ✅ Có | ❌ Không |
| Tự động thử lại | ✅ Có | ❌ Không (cần DLQ) |
| Chi phí | $1.00/M lượt gọi | Miễn phí (5M/tháng đầu) |
| Trường hợp sử dụng | Công việc production | Công việc định kỳ đơn giản |
Ví dụ thực tế: Dọn dẹp cơ sở dữ liệu hàng ngày#
import { DynamoDB } from 'aws-sdk';
const dynamodb = new DynamoDB.DocumentClient();
const TABLE_NAME = 'user-activity';
interface CleanupResult {
deletedCount: number;
cutoffDate: string;
}
export const lambdaHandler = async (): Promise<APIResponse> => {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - 30);
try {
// Quét và xóa các bản ghi cũ
const response = await dynamodb.scan({
TableName: TABLE_NAME,
FilterExpression: 'created_at < :cutoff',
ExpressionAttributeValues: {
':cutoff': cutoffDate.toISOString()
}
}).promise();
let deletedCount = 0;
if (response.Items) {
for (const item of response.Items) {
await dynamodb.delete({
TableName: TABLE_NAME,
Key: { id: item.id }
}).promise();
deletedCount++;
}
}
logger.info(`Đã xóa ${deletedCount} bản ghi cũ`);
return {
statusCode: 200,
body: JSON.stringify({
deletedCount,
cutoffDate: cutoffDate.toISOString()
} as CleanupResult)
};
} catch (error) {
logger.error(`Dọn dẹp thất bại: ${error}`);
throw error;
}
};typescriptLịch trình với EventBridge Scheduler:
# Chạy hàng ngày lúc 2:00 AM UTC
cron(0 2 * * ? *)bashKhắc phục sự cố#
Công việc không chạy#
- Kiểm tra biểu thức lịch trình: Đảm bảo cú pháp cron đúng
- Xác minh quyền IAM: Lambda có quyền cần thiết
- Kiểm tra CloudWatch Logs: Tìm thông báo lỗi
- Xác minh ARN mục tiêu: Đảm bảo ARN hàm Lambda đúng
Công việc chạy nhưng thất bại#
- Xem lại CloudWatch Logs: Tìm thông báo lỗi
- Kiểm tra thời gian chờ: Lambda có thể đang hết thời gian
- Xác minh quyền tài nguyên: Truy cập cơ sở dữ liệu, S3, v.v.
- Kiểm tra cục bộ: Chạy Lambda cục bộ để debug
Các lần thực thi chồng chéo#
- Triển khai cơ chế khóa: Sử dụng DynamoDB hoặc Redis
- Sử dụng concurrency dự phòng: Giới hạn các lần thực thi đồng thời
- Thêm tính định danh: Đảm bảo handler có tính định danh
Dọn dẹp tài nguyên#
Khi không còn cần thiết, dọn dẹp tài nguyên:
# Xóa lịch trình
aws scheduler delete-schedule --name daily-job
# Xóa quy tắc CloudWatch
aws events delete-rule --name daily-lambda-rule
# Xóa hàm Lambda
aws lambda delete-function --function-name my-function
# Xóa vai trò IAM
aws iam delete-role --role-name scheduler-rolebashKết luận#
AWS Lambda kết hợp với EventBridge Scheduler/CloudWatch Events cung cấp giải pháp mạnh mẽ cho các công việc định kỳ:
- EventBridge Scheduler: Cho công việc production với hỗ trợ múi giờ, thử lại, cửa sổ linh hoạt
- CloudWatch Events: Cho công việc định kỳ đơn giản với hiệu quả chi phí
Các điểm chính:
- Sử dụng EventBridge Scheduler cho production
- Triển khai tính định danh và xử lý lỗi
- Thiết lập giám sát với CloudWatch
- Sử dụng DLQ cho các lượt gọi thất bại
- Ngăn chặn các lần thực thi chồng chéo