Giới thiệu
Chào các bạn tới với series về Serverless, ở bài trước chúng ta đã nói về cách sử dụng AWS API Gateway kết hợp với AWS Lambda để xây dựng REST API theo mô hình Serverless. Tuy nhiên, Lambda functions là stateless application nên nó không thể lưu trữ dữ liệu được, nên ở bài này ta sẽ tìm hiểu về thành phần thứ ba để xây dựng mô hình Serverless trên môi trường AWS Cloud, là DynamoDB.
Kết thúc bài trước thì ta đã xây được REST API với API Gateway + Lambda theo minh họa sau đây.
Ở bài này thì mình sẽ dùng Terraform để tạo ra hệ thống ở trên, nên ta không cần phải tạo từ đầu, nếu các bạn muốn biết cách tạo bằng tay theo từng bước thì các bạn đọc ở bài trước nhé. Bước tiếp theo ta cần làm cho hệ thống trên là gắn thêm DynamoDB vào để lưu trữ data, minh họa như sau.
DynamoDB
DynamoDB là database dạng NoSQL, được thiết kế và phát triển bởi AWS. Đây là một trong những service thuộc dạng Serverless của AWS, nó có thể tự động scale tùy thuộc vào dữ liệu ta ghi và đọc vào DB, dữ liệu có thể được lưu trữ dưới dạng encryption để tăng độ bảo mật, có thể tự động backup và restore dữ liệu.
Ta sẽ xem qua một vài khái niệm chính của DynamoDB mà ta cần hiểu trước khi sử dụng nó với Lambda.
Kiến trúc của DynamoDB
Gồm có Table là tập họp của nhiều items (rows), với mỗi item là tập họp của nhiều attributes (columns) và values.
Trong table thì sẽ có primary keys, và primary keys thì có hai loại là:
- Partition key: là một hash key, giá trị của nó sẽ là unique ID trong một bảng.
- Partition key + sort key: là một cặp primary key, với partition key dùng để định nghĩa item đó trong một bảng và sort key dùng để sort item theo partition key.
Ngoài ra trong table còn có Index, thì giống với các loại database khác nó dùng để tăng tốc độ query của một table, có hai loại index là Global Secondary Index (GSI) với Local Secondary Index (LSI).
Tương tác với DynamoDB
Ta sẽ có các operations sau để tương tác với DynamoDB:
- Scan: operation này sẽ duyệt qua toàn bộ table để tìm kiếm item theo điều kiện nào đó.
- Query: operation này sẽ kiếm item theo primary key và trả về một list.
- PutItem: operation này dùng sẽ tạo mới hoặc cập nhật lại một item.
- GetItem: operation này sẽ kiếm item theo primary key và chỉ trả về kết quả đầu tiên.
- DeleteItem: operation này sẽ xóa item trong table dựa vào primary key.
Đây là những hàm cơ bản để ta tương tác với DynamoDB, để hiểu rõ hơn về DynamoDB thì còn rất nhiều thứ để học ?, ở bài này ta chỉ xem qua một số cái cơ bản để ta có thể làm việc với nó, mình cũng không có biết nhiều lắm về DynamoDB ?. Giờ ta sẽ tiến hành tạo bảng và sẽ viết code để Lambda có thể lưu dữ liệu vào trong DynamoDB.
Tạo bảng
Truy cập lên AWS Web Console, kiếm DynamoDB và bấm vào create table, bạn sẽ thấy giao diện sau đây, ở chỗ Table name điền vào là books, ở chỗ Partition key điền vào id.
Các giá trị còn lại bạn để mặc định.
Bấm tạo và chờ một lát và bạn sẽ thấy table của ta.
Ta cũng có thể tạo bằng câu CLI sau đây cho nhanh nếu bạn không muốn dùng UI.
$ aws dynamodb create-table --table-name books --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1
Sau khi tạo bảng xong thì ta sẽ insert vào một vài data mẫu.
Ghi dữ liệu vào DynamoDB
Bấm vào books table, ở mục action, chọn create item.
Xong điền vào giá trị như sau và bấm tạo.
Sau khi tạo xong, kéo xuống mục Item summary, bấm vào View items.
Ta sẽ thấy item ta vừa tạo.
Ta có thể dùng CLI để insert dữ liệu vào bảng, như sau:
$ aws dynamodb put-item --table-name books --item file://item.json
{ "id": { "S": "2" }, "name": { "S": "Golang" }, "author": { "S": "Golang" }
}
Kết quả.
Sau khi ghi dữ liệu mãu vào trong DB xong, bây giờ ta sẽ chuyển sang integrate Lambda với DynamoDB, đầu tiên ta sẽ viết function list dữ liệu trong DynamoDB ra.
Integrate Lambda với DynamoDB
Đầu tiên ta sẽ tạo API Gateway + Lambda function trước, như đã nói ở trên thì ta sẽ dùng terraform, các bạn xem bài 2 để biết cách tạo bằng tay. Các bạn tải source code ở git repo này https://github.com/hoalongnatsu/serverless-series.git, di chuyển tới folder bai-3/terraform-start, mở file policies/lambda_policy.json ra.
{ "Version": "2012-10-17", "Statement": [ { "Sid": "1", "Action": "logs:*", "Effect": "Allow", "Resource": "*" }, { "Sid": "2", "Effect": "Allow", "Action": "dynamodb:*", "Resource": "arn:aws:dynamodb:us-west-2:<ACCOUNT_ID>:table/books" } ]
}
Ở chỗ resource arn:aws:dynamodb:us-west-2:<ACCOUNT_ID>:table/books
, thay ACCOUNT_ID bằng account AWS của bạn. Xong sau đó, bạn chạy câu lệnh:
terraform init
terraform apply -auto-approve
Xong khi tạo xong, bấm vào AWS Lambda và API Gateway, bạn sẽ thấy các function như sau.
API Gateway
Bấm vào books-api và bấm qua mục Stages, ta sẽ thấy URL của API ngay chỗ Invoke URL.
Oke, vậy là ta đã chuẩn bị xong, tiếp theo ta sẽ tiến hành viết code nào. Tạo thư mục như sau.
.
├── create
│ ├── build.sh
│ └── main.go
├── delete
│ ├── build.sh
│ └── main.go
├── get
│ ├── build.sh
│ └── main.go
└── list ├── build.sh └── main.go
#!/bin/bash GOOS=linux go build -o main main.go
zip list.zip main
rm -rf main
Các file build.sh
còn lại các bạn thay tên file zip tương ứng với tên thư mục, ví dụ ở thư mục list thì file zip build ra sẽ là list.zip
. Đầu tiên là sẽ viết code cho API list trước.
List with scan operation
Cập nhật code của file list/main.go như sau:
package main import ( "context" "encoding/json" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb"
) func list(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "Error while retrieving AWS credentials", }, nil } svc := dynamodb.NewFromConfig(cfg) out, err := svc.Scan(context.TODO(), &dynamodb.ScanInput{ TableName: aws.String("books"), }) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: err.Error(), }, nil } res, _ := json.Marshal(out.Items) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{ "Content-Type": "application/json", }, Body: string(res), }, nil
} func main() { lambda.Start(list)
}
Init code và upload code lên AWS Lambda.
go mod init list
go get
sh build.sh
aws lambda update-function-code --function-name books_list --zip-file fileb://list.zip --region us-west-2
Để tương tác được với DynamoDB, ta sẽ sử dụng hai package là github.com/aws/aws-sdk-go-v2/config
và github.com/aws/aws-sdk-go-v2/service/dynamodb
.
Đầu tiên, ta sẽ load config mặc định khi Lambda function được thực thi bằng câu lệnh config.LoadDefaultConfig(context.TODO())
.
Sau đó ta sẽ khởi tạo DynamoDB bằng config trên với câu lệnh dynamodb.NewFromConfig(cfg)
. Để lấy toàn bộ item trong bảng books, ta dùng lệnh scan ở đoạn code sau.
out, err := svc.Scan(context.TODO(), &dynamodb.ScanInput{ TableName: aws.String("books"),
})
Gọi thử API của ta, copy Invoke URL ở API Gateway.
$ curl https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books
[{"author":{"Value":"Golang"},"id":{"Value":"2"},"name":{"Value":"Golang"}},{"author":{"Value":"NodeJS"},"id":{"Value":"1"},"name":{"Value":"NodeJS"}}]
Ta sẽ thấy dữ liệu ở trong bảng books của ta đã được API trả về chính xác, vậy là ta đã kết nối được Lambda function với DynamoDB ?. Nhưng mà kết quả ở trên nó trả về không được đẹp cho lắm, ta sẽ sửa lại để API của ta trả về kết quả với định dạng dễ xài hơn. Ta sẽ dùng package github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue
để format dữ liệu, tải package.
go get github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue
Cập nhật lại file main.go với đoạn code sau
package main import ( "context" "encoding/json" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/service/dynamodb"
) type Book struct { Id string `json:"id"` Name string `json:"name"` Author string `json:"author"`
} func list(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "Error while retrieving AWS credentials", }, nil } svc := dynamodb.NewFromConfig(cfg) out, err := svc.Scan(context.TODO(), &dynamodb.ScanInput{ TableName: aws.String("books"), }) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: err.Error(), }, nil } books := []Book{} err = attributevalue.UnmarshalListOfMaps(out.Items, &books) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "Error while Unmarshal books", }, nil } res, _ := json.Marshal(books) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{ "Content-Type": "application/json", }, Body: string(res), }, nil
} func main() { lambda.Start(list)
}
Ta sẽ format lại dữ liệu trả về với struct Book ở đoạn code.
books := []Books{}
err = attributevalue.UnmarshalListOfMaps(out.Items, &books)
Giờ khi gọi lại API, ta sẽ thấy kết quả trả về với định dạng dễ xài hơn.
$ curl https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books
[{"id":"2","name":"Golang","author":"Golang"},{"id":"1","name":"NodeJS","author":"NodeJS"}]
Get one with GetItem operation
Tiếp theo ta sẽ implement API get one. Cập nhật lại file get/main.go như sau.
package main import ( "context" "encoding/json" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
) type Book struct { Id string `json:"id"` Name string `json:"name"` Author string `json:"author"`
} func get(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "Error while retrieving AWS credentials", }, nil } svc := dynamodb.NewFromConfig(cfg) out, err := svc.GetItem(context.TODO(), &dynamodb.GetItemInput{ TableName: aws.String("books"), Key: map[string]types.AttributeValue{ "id": &types.AttributeValueMemberS{Value: req.PathParameters["id"]}, }, }) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: err.Error(), }, nil } movie := Book{} err = attributevalue.UnmarshalMap(out.Item, &movie) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "Error while marshal movies", }, nil } res, _ := json.Marshal(movie) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{ "Content-Type": "application/json", }, Body: string(res), }, nil
} func main() { lambda.Start(get)
}
Init code và upload lên AWS Lambda.
go mod init get
go get
sh build.sh
aws lambda update-function-code --function-name books_get --zip-file fileb://get.zip --region us-west-2
Để kiếm item theo primary key, ta dùng hàm GetItem với đoạn code:
out, err := svc.GetItem(context.TODO(), &dynamodb.GetItemInput{ TableName: aws.String("books"), Key: map[string]types.AttributeValue{ "id": &types.AttributeValueMemberS{Value: req.PathParameters["id"]}, },
})
Kiểm tra thử API.
$ curl https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books/1
{"id":"1","name":"NodeJS","author":"NodeJS"}
Nếu các bạn in ra được kết quả như trên thì API get one của ta đã chạy đúng. Nếu bạn gọi đến API get one mà với id của item mà không có trong bảng, thì nó sẽ không trả về 404 mà sẽ là một object với giá trị của từng property là trỗng.
$ curl https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books/3
{"id":"","name":"","author":""}
Nếu bạn muốn trả về lỗi 404 thì ta có thể làm bằng tay.
Create with with PutItem operation
Tiếp theo ta sẽ làm API create, cập nhật code ở file create/main.go như sau:
package main import ( "context" "encoding/json" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
) type Book struct { Id string `json:"id"` Name string `json:"name"` Author string `json:"author"`
} func create(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { var book Book err := json.Unmarshal([]byte(req.Body), &book) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: 400, Body: err.Error(), }, nil } cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "Error while retrieving AWS credentials", }, nil } svc := dynamodb.NewFromConfig(cfg) newBook, err := svc.PutItem(context.TODO(), &dynamodb.PutItemInput{ TableName: aws.String("books"), Item: map[string]types.AttributeValue{ "id": &types.AttributeValueMemberS{Value: book.Id}, "name": &types.AttributeValueMemberS{Value: book.Name}, "author": &types.AttributeValueMemberS{Value: book.Author}, }, ReturnValues: types.ReturnValueAllOld, }) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: err.Error(), }, nil } res, _ := json.Marshal(newBook) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{ "Content-Type": "application/json", }, Body: string(res), }, nil
} func main() { lambda.Start(create)
}
Init code và upload lên AWS Lambda.
go mod init create
go get
sh build.sh
aws lambda update-function-code --function-name books_create --zip-file fileb://create.zip --region us-west-2
Để insert dữ liệu được vào DynamoDB, ta dùng hàm PutItem với đoạn code:
newBook, err := svc.PutItem(context.TODO(), &dynamodb.PutItemInput{ TableName: aws.String("books"), Item: map[string]types.AttributeValue{ "id": &types.AttributeValueMemberS{Value: book.Id}, "name": &types.AttributeValueMemberS{Value: book.Name}, "author": &types.AttributeValueMemberS{Value: book.Author}, }, ReturnValues: types.ReturnValueAllOld,
})
Kiểm tra thử API create của ta.
$ curl -sX POST -d '{"id":"3", "name": "Java", "author": "Java"}' https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books
{"Attributes":null,"ConsumedCapacity":null,"ItemCollectionMetrics":null,"ResultMetadata":{}}
Oke, sau đó ta gọi lại API list.
$ curl https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books
[{"id":"3","name":"Java","author":"Java"},{"id":"2","name":"Golang","author":"Golang"},{"id":"1","name":"NodeJS","author":"NodeJS"}]
Bạn sẽ thấy dữ liệu ta mới insert vào database bằng API create ở trên đã xuất hiện, vậy là API create của ta dã hoạt động đúng.
Delete with DeleteItem operation
Tiếp theo ta sẽ làm API delete, cập nhật code ở file delete/main.go như sau:
package main import ( "context" "encoding/json" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
) type Book struct { Id string `json:"id"` Name string `json:"name"` Author string `json:"author"`
} func get(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "Error while retrieving AWS credentials", }, nil } svc := dynamodb.NewFromConfig(cfg) out, err := svc.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{ TableName: aws.String("books"), Key: map[string]types.AttributeValue{ "id": &types.AttributeValueMemberS{Value: req.PathParameters["id"]}, }, }) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: err.Error(), }, nil } res, _ := json.Marshal(out) return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{ "Content-Type": "application/json", }, Body: string(res), }, nil
} func main() { lambda.Start(get)
}
Init code và upload lên AWS Lambda.
go mod init delete
go get
sh build.sh
aws lambda update-function-code --function-name books_delete --zip-file fileb://delete.zip --region us-west-2
Để delete dữ liệu trong DynamoDB, ta dùng hàm DeleteItem với đoạn code:
out, err := svc.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{ TableName: aws.String("books"), Key: map[string]types.AttributeValue{ "id": &types.AttributeValueMemberS{Value: req.PathParameters["id"]}, },
})
Kiểm tra API delete của ta.
$ curl -sX DELETE -d '{"id":"3"}' https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books
$ $ curl https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books
[{"id":"2","name":"Golang","author":"Golang"},{"id":"1","name":"NodeJS","author":"NodeJS"}]
Oke, API delete của ta đã hoạt động đúng ?.
Kết luận
Vậy là ta đã tìm hiểu xong cách integrate Lambda với DynamoDB. Nếu có thắc mắc hoặc cần giải thích rõ thêm chỗ nào thì các bạn có thể hỏi dưới phần comment. Hẹn gặp mọi người ở bài tiếp theo.