Gần đây, tôi đã khám phá Amazon Aurora DSQL — cơ sở dữ liệu SQL phân tán mới nhất của AWS được xây dựng dành riêng cho các khối lượng công việc serverless. Không giống như các cơ sở dữ liệu truyền thống cần bảo trì thường xuyên, Aurora DSQL tự động mở rộng và chỉ tính phí dựa trên mức sử dụng — khiến nó trở thành một lựa chọn hoàn hảo cho các hàm Lambda.
Khi nhắc đến cơ sở dữ liệu serverless, nhiều người nghĩ ngay đến DynamoDB — và điều đó hoàn toàn hợp lý. Nó đã là giải pháp NoSQL hàng đầu cho các ứng dụng serverless suốt nhiều năm. Nhưng giờ đây, chúng ta cuối cùng cũng có thể cân nhắc sử dụng cơ sở dữ liệu quan hệ trong bối cảnh serverless thực sự. Aurora DSQL mang lại sự quen thuộc của SQL và mô hình dữ liệu quan hệ vào thế giới serverless, mà không cần lo lắng về việc quản lý instance hoặc vấn đề mở rộng.
Bài viết này sẽ đề cập đến những gì?
Thiết lập dự án:
Bạn sẽ học cách khởi tạo và tổ chức một dự án backend serverless, bao gồm thiết lập repository và chuẩn bị các file cũng như thư mục cần thiết để có workflow phát triển mượt mà.
AWS CDK cho hạ tầng:
Bài viết hướng dẫn cách sử dụng AWS CDK để định nghĩa hạ tầng cloud bằng mã. Bạn sẽ biết cách dùng các construct của CDK (đặc biệt là L1 với Aurora DSQL) để triển khai tài nguyên như Lambda và Aurora DSQL, cũng như cấu trúc stack cho dễ bảo trì và mở rộng.
Kết nối Lambda và DSQL:
Bạn sẽ biết cách thiết lập kết nối an toàn giữa Lambda và Aurora DSQL. Bao gồm việc cấu hình IAM role, policy cho Lambda để tương tác với DSQL Data API, thiết lập biến môi trường hoặc secrets để truy cập CSDL một cách bảo mật.
Đoạn mã từng bước:
Cung cấp ví dụ mã cụ thể ở mỗi bước. Bạn sẽ thấy cách viết CDK stack, định nghĩa Lambda function và kết nối với DSQL, dễ dàng làm theo và áp dụng cho dự án của mình.
Repository mã nguồn mở:
Bài viết có liên kết đến một repo open-source, giúp bạn dễ dàng xem cấu trúc dự án đầy đủ, clone mã về và tự thử nghiệm. Rất minh bạch và tiện mở rộng.
Bước 1: Tổ chức dự án
Cấu trúc thư mục
Với:
infra/
- Mã CDK định nghĩa hạ tầngservice/
- Mã nguồn của Lambda.github/
- Workflow của GitHub Actions
Lưu ý:
- Cách tổ chức này giúp tôi tách biệt rõ ràng hạ tầng, logic nghiệp vụ và các hành động liên quan đến CI/CD pipeline.
- Ngoài ra, tôi thích chia hạ tầng thành các stack theo từng domain vì điều đó rất hữu ích khi dự án phát triển lớn hơn.
Bước 2: Làm việc với AWS CDK và DSQL
Ghi chú: Aurora DSQL mới ra mắt nên chỉ có L1 Construct (được sinh tự động). Tôi sẽ cập nhật bài viết và repo khi AWS hỗ trợ tốt hơn.
Ví dụ khởi tạo DSQL Cluster:
export class StorageStack extends cdk.Stack { dsqlCluster: cdk.CfnResource; dsqlClusterArn: string; dsqlClusterEndpoint: string; constructor(scope: constructs.Construct, id: string, props: cdk.StackProps) { super(scope, id, props); // Create DSQL cluster using native CloudFormation resource this.dsqlCluster = new cdk.CfnResource(this, 'DSQLCluster', { type: 'AWS::DSQL::Cluster', properties: { DeletionProtectionEnabled: true, Tags: [ { Key: 'Project', Value: 'aws-dsql-demo' } ] } }); this.dsqlClusterArn = this.dsqlCluster.getAtt('ResourceArn').toString(); this.dsqlClusterEndpoint = `${this.dsqlCluster.getAtt('Identifier').toString()}.dsql.${this.region}.on.aws`; }
}
Bước 3: Tạo Lambda và phân quyền
export interface FunctionsStackProps extends cdk.StackProps { label: { id: string; }, domainName: string; dsqlClusterEndpoint: string; dsqlClusterArn: string;
} export class FunctionsStack extends cdk.Stack { generateGameLambda: lambda.Function; constructor(scope: constructs.Construct, id: string, props: FunctionsStackProps) { super(scope, `${id}-functions-stack`, props); // Create Lambda role with DSQL permissions const lambdaRole = new iam.Role(this, 'DSQLLambdaRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), ] }); // Add DSQL permissions lambdaRole.addToPolicy(new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'dsql:DbConnect', 'dsql:ExecuteStatement', 'dsql:DbConnectAdmin', ], resources: [props.dsqlClusterArn] })); this.generateGameLambda = new nodeLambda.NodejsFunction(this, 'GenerateGameLambda', { entry: './service/generate-game/Handler.ts', runtime: lambda.Runtime.NODEJS_20_X, role: lambdaRole, environment: { DSQL_ENDPOINT: props.dsqlClusterEndpoint } }); }
}
Bước 4: Tối ưu kết nối database trong Lambda
Tạo lớp DatabaseService
để:
- Cache kết nối
TypeORM DataSource
trong lifecycle của Lambda. - Sử dụng
DsqlSigner
để tạo token tạm thời. - Dùng
TypeORM
để quản lý entity.
import "reflect-metadata";
import { DataSource } from "typeorm";
import { DsqlSigner } from "@aws-sdk/dsql-signer";
import { Game } from "../models/Game"; export class DatabaseService { private static dataSource: DataSource; private static async getAuthToken(host: string): Promise<string> { const signer = new DsqlSigner({ hostname: host, region: process.env.AWS_REGION || 'eu-west-2' }); return await signer.getDbConnectAdminAuthToken(); } static async initialize(): Promise<DataSource> { if (!DatabaseService.dataSource) { const host = process.env.DSQL_ENDPOINT || ''; DatabaseService.dataSource = new DataSource({ type: "postgres", host: host, port: 5432, username: 'admin', password: await DatabaseService.getAuthToken(host), database: "postgres", ssl: { rejectUnauthorized: true }, synchronize: true, logging: true, entities: [Game] }); // Initialize connection to postgres database await DatabaseService.dataSource.initialize(); } return DatabaseService.dataSource; } static async saveGame(game: Partial<Game>): Promise<Game> { const dataSource = await DatabaseService.initialize(); const gameRepository = dataSource.getRepository(Game); const newGame = gameRepository.create(game); return await gameRepository.save(newGame); }
}
Bước 5: Tạo handler cho Lambda
export async function handler(event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> { console.log(event, context); await DatabaseService.initialize(); try { // Generate a random game const gameData = generateRandomGame(); // Save it to the database const savedGame = await DatabaseService.saveGame(gameData); return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Game generated and saved successfully', game: savedGame }) }; } catch (error) { console.error('Error generating game:', error); return { statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Failed to generate and save game', error: error instanceof Error ? error.message : String(error) }) }; }
}
Debug thủ công
Bạn có thể kết nối trực tiếp với CSDL thông qua AWS Console:
- Tìm kiếm “AWS DSQL”
- Chọn Cluster
- Nhấn Connect > Open in CloudShell
- Chọn kết nối với quyền admin và chạy truy vấn SQL
Bạn sẽ làm được những gì qua bài viết này:
- Thiết lập cấu trúc dự án rõ ràng, tách biệt giữa hạ tầng, code dịch vụ và CI/CD
- Khởi tạo Aurora DSQL Cluster và Lambda bằng AWS CDK (sử dụng L1 construct)
- Cấu hình quyền IAM cho Lambda kết nối và thực thi truy vấn trên Aurora DSQL
- Tối ưu kết nối DB với TypeORM + DsqlSigner để bảo mật token
- Tạo Lambda handler lưu dữ liệu vào Aurora DSQL và cung cấp qua API Gateway