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ề kiến trúc Serverless là gì, AWS Lambda là gì và nó đóng vai trò như thế nào trong mô hình Serverless. Ở bài này chúng ta sẽ tìm hiểu về thành phần thứ hai để ta xây dựng mô hình Serverless trên môi trường AWS Cloud, là API Gateway. Ta sẽ sử dụng API Gateway kết hợp với Lambda để xây dựng một REST API theo mô hình Serverless.
Hình minh họa REST API mà ta sẽ xây dựng.
API Gateway
Như đã nói ở bài trước, AWS Lambda function của ta sẽ không thể tự động chạy, mà nó sẽ được thực thi bởi một event nào đó. Thì API Gateway là một trong những serivce mà sẽ phát ra event để thực thi Lambda function, cụ thể hơn là API Gateway sẽ phát ra một event tới Lambda function khi có một http request từ phía người dùng gọi tới nó, do đó nó rất thích hợp cho việc xây dựng REST API.
Trong mô hình Serverless thì API Gateway sẽ đóng vai trò như là một entry point cho toàn bộ Lambda functions của ta, nó sẽ proxy và điều hướng một http request tới đúng Lambda function mà ta muốn.
Bên cạnh việc đóng vai trò như một entry point cho Lambda function, API Gateway còn có những đặc tính nổi bật sau đây:
- Caching: Ta có thể cache lại kết quả mà Lambda trả về => giảm số lần mà API Gateway gọi tới Lambda function bên dưới, giúp ta giảm tiền và giảm thời gian response của một request.
- Cấu hình CORS.
- Deployment stages: API Gateway hỗ trợ tạo và quản lý các version khác nhau của API, do đó ta có thể chia ra được nhiều môi trường (dev, staging, production).
- Hỗ trợ monitor và debug ở tầng http request
- Hỗ trợ ra tạo ra document một cách dễ dàng, như là export API ra theo dạng docs mà Swagger có thể đọc được
Ta chỉ nói lý thuyết nhiêu đây thôi, tiếp theo ta sẽ bắt tay vào xây dựng REST API.
Xây dựng REST API
Ta sẽ làm một REST API mà thực hiện CRUD đơn giản, gồm list books, get one books, create book, update book và delete book. Ở chương này ta chỉ tương tác với dữ liệu giả được gán cứng vào biến, ta chưa có tương tác với database nha.
Khởi tạo Lambda function
Bước đầu tiên ta sẽ tạo một lambda fuction mà trả về list books.
Mình sẽ dùng terraform để tạo lambda function, nếu các bạn chưa quen với terraform thì xem bài trước để biết cách tạo bằng AWS Web Console nhé, còn các bạn muốn tìm hiểu về Terraform thì mình cũng có viết series về Terraform, các bạn có thể đọc để biết thêm. Các bạn tải code ở repo github này https://github.com/hoalongnatsu/serverless-series.git, nhảy vào folder bai-2/terraform-start. Chạy những câu lệnh sau terraform init
-> terraform apply -auto-approve
. Sau khi chạy xong bạn sẽ thấy trên một lambda function tên là books_list được tạo ra trên AWS.
Bây giờ ta sẽ viết code cho function list, tạo một folder tên list, mở nó ra và tạo một file tên là main.go với code sau đây.
package main import ( "encoding/json" "github.com/aws/aws-lambda-go/lambda"
) type Books struct { Id int `json:"id"` Name string `json:"name"` Author string `json:"author"`
} func list() (string, error) { books := []Books{ {Id: 1, Name: "NodeJS", Author: "NodeJS"}, {Id: 2, Name: "Golang", Author: "Golang"}, } res, _ := json.Marshal(&books) return string(res), nil
} func main() { lambda.Start(list)
}
Sau khi viết code xong, ta chạy câu lệnh sau để build golang code ra file binary và upload nó lên lambda function. Tải package go mod init list && go get
và build code:
go build -o main main.go
zip list.zip main
rm -rf main
Để tiện cho sau này build thì bạn tao một file tên là build.sh
, copy đoạn code trên vào. Folder của ta sau lúc này sẽ như sau:
.
├── build.sh
├── go.mod
├── go.sum
├── list.zip
└── main.go
Sau khi build ra được file list.zip
, ta sẽ update lại books_list lambda function.
$ aws lambda update-function-code --function-name books_list --zip-file fileb://list.zip --region us-west-2
Khi upload code xong ta kiểm tra lại xem lambda function của ta có chạy đúng không.
$ aws lambda invoke --function-name books_list response.json --region us-west-2
{ "StatusCode": 200, "ExecutedVersion": "$LATEST"
} $ cat response.json ; echo
"[{\"id\":1,\"name\":\"NodeJS\",\"author\":\"NodeJS\"},{\"id\":2,\"name\":\"Golang\",\"author\":\"Golang\"}]"
Nếu bạn thấy kết quả trên thì lambda function của ta đã chạy đúng. Tiếp theo, thay vì thực thi function bằng CLI, ta sẽ dùng API Gateway để thực thi nó.
Khởi tạo API Gateway
Mở Web Console và kiếm API Gateway.
Chọn REST API không có private.
Chỗ protocol ta chọn REST, chọn New API, chỗ API name bạn nhập gì cũng được, của mình thì mình nhập là BOOKS. Endpoint Type ta chọn Regional.
Nhập xong hết thì ta bấm Create API và ta sẽ qua trang có UI như sau.
Giờ ta sẽ định nghĩa API của ta. Bấm vào chỗ Actions, chọn Create Resource.
Chỗ Resource Name và Resource Path, ta nhập vào là books. Nhấn Create Resource.
Sau khi tạo xong, bạn sẽ thấy chỗ Resource có thêm path là /books
.
API List
Giờ ta sẽ tạo method chỗ Resource này, nhấn vào Actions, chọn Create Method.
Sau đó nó sẽ hiện một ô dropdown cho bạn, ta chọn GET và nhấn nút check.
Nó sẽ hiện UI như sau, bạn chọn như hình phía dưới và ở ô nhập vào Lambda Function, bạn nhập vào books_list.
Và nhấn Save. Sẽ có một modal mở lên nói là nó sẽ tạo permission để API Gateway có thể thực thi được Lambda function, ta chọn OK.
Tiếp theo ta sẽ deploy REST API của ta, chỗ Actions, chọn Deploy API.
Ta chọn New Stage, chỗ Stage name nhập vào staging (này bạn nhập gì cũng được).
Và bấm Deploy. Và ta sẽ có UI như sau.
Mở staging ra, chọn vào GET method, ta sẽ thấy được URL của API mà thực hiện get list books cho ta.
Oke, vậy là ta đã deploy được REST API đầu tiên của ta ?. Bạn copy URL và thực hiện gửi request tới nó.
$ curl https://ferwqd3ttf.execute-api.us-west-2.amazonaws.com/staging/books ; echo
{"message": "Internal server error"}
Và ta sẽ thấy có lỗi xảy ra ?. Lý do là vì để Lambda kết hợp được với API Gateway, thì Lambda function phải trả về đúng format mà API Gateway quy định. Ở đoạn code trên ta trả về kết quả chưa đúng format.
...
func list() (string, error) { books := []Books{ {Id: 1, Name: "NodeJS", Author: "NodeJS"}, {Id: 2, Name: "Golang", Author: "Golang"}, } res, _ := json.Marshal(&books) return string(res), nil // response not valid format of API Gateway
}
...
Format đúng của response mà Lambda function trả về cho API Gateway sẽ như sau:
type Response struct { StatusCode int `json:"statusCode"` Body string `json:"body"`
}
Nó sẽ gồm một trường statusCode định dạng số và một trường body định dạng là string. Ta cập nhật lại file main.go
package main import ( "encoding/json" "github.com/aws/aws-lambda-go/lambda"
) type Response struct { StatusCode int `json:"statusCode"` Body string `json:"body"`
} type Books struct { Id int `json:"id"` Name string `json:"name"` Author string `json:"author"`
} func list() (Response, error) { books := []Books{ {Id: 1, Name: "NodeJS", Author: "NodeJS"}, {Id: 2, Name: "Golang", Author: "Golang"}, } res, _ := json.Marshal(&books) return Response{ StatusCode: 200, Body: string(res), }, nil
} func main() { lambda.Start(list)
}
Build code và upload lại lên AWS.
$ sh build.sh
updating: main (deflated 46% $ aws lambda update-function-code --function-name books_list --zip-file fileb://list.zip --region us-west-2
Giờ ta gọi lại API list books, ta sẽ thấy được kết quả trả về mà không có lỗi xảy ra.
$ curl https://ferwqd3ttf.execute-api.us-west-2.amazonaws.com/staging/books ; echo
[{"id":1,"name":"NodeJS","author":"NodeJS"},{"id":2,"name":"Golang","author":"Golang"}]
Oke, bây giờ thì REST API đầu tiên của ta mới thực sự được deploy thành công. Để làm đúng thì AWS có cũng cấp cho ta bộ Golang SDK để ta tránh được mấy lỗi trên và viết code nhanh hơn. Thay vì phải tự tạo struct Response , thì ta xài SDK như sau, tải package go get github.com/aws/aws-lambda-go/events
, update lại main.go, ta sử dụng struct có sẵn là APIGatewayProxyResponse.
package main import ( "encoding/json" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda"
) type Books struct { Id int `json:"id"` Name string `json:"name"` Author string `json:"author"`
} func list() (events.APIGatewayProxyResponse, error) { books := []Books{ {Id: 1, Name: "NodeJS", Author: "NodeJS"}, {Id: 2, Name: "Golang", Author: "Golang"}, } 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)
}
Minh họa Serverless hiện tại của ta.
API get one
Tiếp theo ta sẽ làm API get one book theo id. Handle function của lambda khi kết hợp với API sẽ có params đầu tiên được truyền vào là APIGatewayProxyRequest, nó sẽ chứa giá trị request của user, ta sẽ định nghĩa đường dẫn của API get one là /books/{id}, trong đó id là param, và nó sẽ nằm trong trường PathParameters của APIGatewayProxyRequest. Ta tạo một folder tên là getOne, mở nó ra và tạo một file main.go với code như sau:
package main import ( "encoding/json" "strconv" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda"
) type Books struct { Id int `json:"id"` Name string `json:"name"` Author string `json:"author"`
} func getOne(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { books := []Books{ {Id: 1, Name: "NodeJS", Author: "NodeJS"}, {Id: 2, Name: "Golang", Author: "Golang"}, } id, err := strconv.Atoi(req.PathParameters["id"]) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: 400, Body: err.Error(), }, nil } res, err := json.Marshal(books[id-1]) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: 500, Body: err.Error(), }, nil } return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{ "Content-Type": "application/json", }, Body: string(res), }, nil
} func main() { lambda.Start(getOne)
}
Tải package và build source.
go mod init getone
go get
#!/bin/bash GOOS=linux go build -o main main.go
zip getOne.zip main
rm -rf main
sh build.sh
Tạo Lambda function cho API get one.
$ aws lambda create-function --function-name books_get_one --zip-file fileb://getOne.zip --runtime go1.x --handler main --role arn:aws:iam::ACCOUNT_ID:role/lambda_role --region us-west-2
Với arn:aws:iam::ACCOUNT_ID:role/lambda_role
, giá trị ACCOUNT_ID là account id của bạn.
Giờ ta sẽ tạo API cho get one. Quay lại API Gateway, bấm vào /books, chọn Actions -> Create Resource.
Nhập vào {id} và bấm tạo.
Và tạo method cho {id}, chọn GET.
Ở chỗ tên function ta nhập vào books_get_one, bấm tạo và nó sẽ hỏi tạo permission, ta OK.
Ta bấm deploy lại.
Lần này ta ở Deployment stage chọn lại staging trước đó và nhấn Deploy.
Và ta sẽ thấy URL API get one của ta.
Thực hiện request tới nó.
$ curl https://ferwqd3ttf.execute-api.us-west-2.amazonaws.com/staging/books/1 ; echo
{"id":1,"name":"NodeJS","author":"NodeJS"}
Oke, ta đã deploy get one API thành công. Bây giờ Serverless của ta sẽ như sau.
API create
Tiếp theo là API tạo book mới, do ta không có dùng API nên ta chỉ đơn giản là lấy body từ request của client rồi append vào mảng hiện tại. Tạo một folder tên là create, mở nó ra và tạo một file tên là main.go với code như sau:
package main import ( "encoding/json" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda"
) type Books struct { Id int `json:"id"` Name string `json:"name"` Author string `json:"author"`
} var books = []Books{ {Id: 1, Name: "NodeJS", Author: "NodeJS"}, {Id: 2, Name: "Golang", Author: "Golang"},
} func create(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { var book Books err := json.Unmarshal([]byte(req.Body), &book) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: 400, Body: err.Error(), }, nil } books = append(books, book) res, err := json.Marshal(&books) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: 500, Body: err.Error(), }, nil } return events.APIGatewayProxyResponse{ StatusCode: 200, Headers: map[string]string{ "Content-Type": "application/json", }, Body: string(res), }, nil
} func main() { lambda.Start(create)
}
Tải package và build source.
go mod init create
go get
#!/bin/bash GOOS=linux go build -o main main.go
zip create.zip main
rm -rf main
sh build.sh
Tạo Lambda function cho API create.
$ aws lambda create-function --function-name books_create --zip-file fileb://create.zip --runtime go1.x --handler main --role arn:aws:iam::ACCOUNT_ID:role/lambda_role --region us-west-2
Ở API Gateway, chọn lại mục Resource, bấm vào /books, chọn Actions -> Create Resource, xong đó ở chỗ dropdown ta chọn POST method.
Ở ô nhập function, ta nhập vào books_create.
Bấm save và chọn OK khi nó hỏi tạo permission. Sau đó ta deploy lại.
Chọn staging như trước đó và nhấn Deploy, và ta sẽ có URL cho API create.
Gửi request tới nó.
$ curl -sX POST -d '{"Id":3, "name": "Java", "author": "Java"}' https://ferwqd3ttf.execute-api.us-west-2.amazonaws.com/staging/books ; echo
[{"id":1,"name":"NodeJS","author":"NodeJS"},{"id":2,"name":"Golang","author":"Golang"},{"id":3,"name":"Java","author":"Java"}]
Oke, API create của ta cũng đã được deploy thành công. Minh họa Serverless hiện tại.
Warm start - Cold start
Ta gọi thêm một lần nữa, bạn sẽ thấy là giá trị được append vào mảng hiện tại và có chứa cả giá trị ta gửi lên lần trước.
$ curl -sX POST -d '{"Id":4, "name": ".NET", "author": ".NET"}' https://ferwqd3ttf.execute-api.us-west-2.amazonaws.com/staging/books ; echo
[{"id":1,"name":"NodeJS","author":"NodeJS"},{"id":2,"name":"Golang","author":"Golang"},{"id":3,"name":"Java","author":"Java"},{"id":4,"name":".NET","author":".NET"}]
Nhưng khi ta đợi thời gian khoảng chừng một 5 phút sau, bạn gọi lại là sẽ thấy là hai giá trị trước đó ta gửi lên bị mất đi.
$ curl -sX POST -d '{"Id":5, "name": "PHP", "author": "PHP"}' https://ferwqd3ttf.execute-api.us-west-2.amazonaws.com/staging/books ; echo
[{"id":1,"name":"NodeJS","author":"NodeJS"},{"id":2,"name":"Golang","author":"Golang"},{"id":5,"name":"PHP","author":"PHP"}]
Tại sao lại như vậy? Thì vấn để này được gọi là Warm start và Cold start trong AWS Lambda. Khi một AWS Lambda thực thi một lambda function, thì nó sẽ làm các bước sau đây:
- Khi function được thực thi lần đầu tiên, AWS Lambda sẽ kiếm chỗ nào đó trên hệ thống máy ảo bên dưới của nó mà có đủ resource để chạy function này, sau khi kiếm được, nó sẽ tạo một container với môi trường phù hợp cho function của ta, sau khi container được tạo xong thì function sẽ được thực thi trong container đó và trả về kết quả cho client, quá trình này gọi là Cold start.
- Và lần sau khi function được thực thi, AWS Lambda sẽ kiểm tra trước đó có container cho function này chưa, nếu có rồi thì nó sẽ sử dụng container cũ để thực thi function, quá trình này gọi là Warm start.
Nhưng nếu một khoảng thời gian mà function không được trigger nữa, container đó sẽ bị xóa đi, và quá trình Cold start sẽ được lặp lại. Đây là lý do vì sao Lambda function là một stateless application, nó không có dùng để lưu dữ liệu được, mà ta phải dùng một service khác để lưu dữ liệu cho ta, như là AWS RDS hoặc DynamoDB.
Các API khác như update và delete thì các bạn làm tương tự nhé. Toàn bộ code của chương này nằm ở repo github ở trên nha.
Kết luận
Vậy là ta đã tìm hiểu xong về các xây dựng REST API theo mô hình Serverless với API Gateway và AWS Lambda. Nếu các bạn thấy chỉ làm một vài API đơn giản mà gì đâu phải tạo nhiều thứ quá thì là do ở đây mình làm bằng tay thôi với do mình giải thích từng cái nên nó sẽ chậm, còn làm thực tế thì ta sẽ có tool để tự động deploy như terraform, viết luồn CI/CD để tự động deploy code, dev local với AWS Serverless Application Model (SAM), những thứ này mình sẽ nói ở các bài sau. 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. Bài tiếp theo ta sẽ nói về cách sử dụng Lambda với DynamoDB.