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 gửi notification tới user khi CI/CD chạy xong. Ở bài này, chúng ta sẽ tìm hiểu về cách làm sao để debug AWS Lambda thông qua AWS CloudWatch, monitoring Lambda dùng metrics có sẵn của CloudWatch + custom metrics mà ta tự định nghĩa, và cách để tracing API dùng AWS X-RAY.
Hệ thống mà ta sẽ xây dựng như sau.
Provisioning previous system
Mình sẽ dùng terraform để tạo lại hệ thống, nếu các bạn muốn biết cách tạo bằng tay từng bước thì các bạn xem từ bài 2 nhé. 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-10/terraform-start. Ở file policies/lambda_policy.json, dòng "Resource": "arn:aws:dynamodb:us-west-2:<ACCOUNT_ID>:table/books", cập nhật lại <ACCOUNT_ID> với account id của bạn.
Nếu các các muốn enable CI/CD thì ở file vars.tf
các bạn sẽ thấy có một biến là codestar_connection, ta cần cập nhật lại giá trị cho biến này. Vì resource này cần phải có thêm bước authentication bằng tay nữa, nên ta không sử dụng Terraform để tự động provisioning nó được, các bạn đọc hướng dẫn ở bài 8 để tạo connection, sau đó copy ARN của nó và dán vào giá trị default của biến codestar_connection.
variable "enable_cicd" {
type = bool
default = true
}
variable "codestar_connection" {
type = string
default = "arn:aws:codestar-connections:<region>:<ACCOUNT_ID>:connection/<id>"
}
Nếu các bạn muốn bật notification tới slack, các bạn sẽ cần cập nhật biến enable_notification và chatbot_arn. Vì chatbot arn chỉ đang ở phiên bản beta nên terraform chưa có hỗ trợ, để tạo chatbot arn các bạn xem ở bài 9, sau đó copy giá trị vào.
variable "enable_notification" {
type = bool
default = true
}
variable "chatbot_arn" {
type = string
default = "arn:aws:chatbot::<ACCOUNT_ID>:chat-configuration/slack-channel/codepipeline_alert"
}
Xong sau đó chạy câu lệnh.
terraform init
terraform apply -auto-approve
Sau khi Terraform chạy xong, nó sẽ tạo ra Lambda list function.
Và in ra terminal URL của API Gateway.
base_url = {
"api_production" = "https://x618g5ucq7.execute-api.us-west-2.amazonaws.com/production"
"api_staging" = "https://x618g5ucq7.execute-api.us-west-2.amazonaws.com/staging"
}
CloudWatch
AWS CloudWatch là một dịch vụ cung cấp khả năng monitoring + observability + logging cho các dịch vụ khác ở trên AWS. Các dịch vụ mà ta xài trên AWS sẽ cung cấp metrics cho CloudWatch, và ta có thể truy cập CloudWatch Dashboard để xem các thông số tài nguyên của các dịch vụ mà ta đang xài.
Ngoài ra CloudWatch còn có một chức năng nữa là lưu trữ log, thông thường thiết kế một hệ thống logging thì cũng khá phức tạp, thay vì phải tự xây dựng thì ta có thể sử dụng CloudWatch Logs như một giải pháp.
Debugging with CloudWatch Logs
Lamda logging
Khi làm việc với AWS Lambda, một số lỗi mà ta có thể gặp ra là:
- Application error.
- Permissions denied.
- Timeout exceeded.
- Memory exceeded.
Ngoài trừ lỗi thứ nhất, thì với các lỗi còn lại ta có thể dễ dàng fix bằng cách thêm permissions, tăng thời gian timeout hoặc memory của function lên. Còn đối với lỗi đầu tiên, ta muốn debug được thì ta cần phải có một chỗ nào đó để ta xem lại log lỗi của thằng Lambda khi nó chạy fail. Thì chỗ lưu log đó là CloudWatch Logs, khi một Lamda được thực thi thì tất cả những log mà nó ghi ra sẽ được lưu ở CloudWatch Logs.
Ta gọi vào api staging ở trên.
curl https://x618g5ucq7.execute-api.us-west-2.amazonaws.com/staging/books
Và để xem log của Lambda list function, ta làm như sau:
- Truy cập vào CloudWtach Console https://console.aws.amazon.com/cloudwatch/home
- Truy cập menu Logs -> Log group.
- Ta sẽ thấy có một log group là /aws/lambda/books_list. Log group của từng function sẽ được lưu dưới dạng
/aws/lambda/<LAMBDA_FUNCTION_NAME>
- Bấm vào nó, ở tab Log streams ta bấm vào log stream mới nhất, ta sẽ thấy được log của lambda function book list.
Oke, để kiểm tra là khi function này có lỗi hoặc khi ta ghi log ra thì nó có được lưu vào CloudWatch Logs hay không, ta sửa lại code của hàm book list như sau. Sửa lại file main.go ở trong github repo của bạn khi mà bạn enable CI/CD khi chạy Terraform ở trên.
package main
import (
"context"
"encoding/json"
"log"
"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"`
Image string `json:"image"`
}
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("book"),
})
if err != nil {
log.Fatal(err.Error());
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",
"Access-Control-Allow-Origin": "*",
},
Body: string(res),
}, nil
}
func main() {
lambda.Start(list)
}
Chỗ mà ta sửa lại là.
...
svc := dynamodb.NewFromConfig(cfg)
out, err := svc.Scan(context.TODO(), &dynamodb.ScanInput{
TableName: aws.String("book"),
})
if err != nil {
log.Fatal(err.Error())
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: err.Error(),
}, nil
}
...
DynamoDB mà ta kết nối tới tên là books, vì để nó quăng ra lỗi nên ta sẽ sửa thành book, giờ ta sẽ update lại Lambda để xem nó có ghi lại lỗi ở trên tới CloudWath không. Commit code và push nó lên trên github repo của bạn, Lambda của ta sẽ tự động cập nhật.
git checkout staging
git commit -am "update list function"
git push
Còn nếu các bạn không có enable CI/CD, thì clone code ở github repo này xuống https://github.com/hoalongnatsu/codepipeline-list-function, cập nhật file main.go, sau đó ta chạy câu lệnh CLI để cập nhật Lambda như sau.
go get
sh build.sh
aws lambda update-function-code --function-name books_list --zip-file fileb://list.zip --region us-west-2
Oke, giờ ta gọi lại api staging thì ta sẽ thấy lỗi.
$ curl https://x618g5ucq7.execute-api.us-west-2.amazonaws.com/staging/books
{"message": "Internal server error"}
Kiểm tra log ở trên CloudWatch. Lúc này bạn sẽ thấy có 1 log stream nữa.
Mỗi lần ta cập nhật lại Lambda thì CloudWatch sẽ tạo ra một log stream mới cho nó, ta kiểm tra log stream mới nhất thì ta sẽ thấy lỗi của Lambda function mà ta vừa cập nhật.
Oke, nó đã in ra lỗi đúng với mục đích của ta. Nhưng các bạn có để ý một điểm là ở chỗ lỗi, ta có trả về lỗi cho client giống như khi ta ghi vào CloudWatch, nhưng kết quả lỗi trả về lại là {"message": "Internal server error"}
không?
...
if err != nil {
log.Fatal(err.Error())
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: err.Error(),
}, nil
}
...
Khi ta kết hợp API Gateway với Lambda thì ta sẽ có điểm lưu ý là khi API Gateway nó trigger Lambda thì ngoài trừ Lambda ghi log của riêng nó, API Gateway cũng có ghi log lại là trong quá trình trigger đó thì nó có xảy ra vấn đề gì hay không, ví dụ như là lambda function trả về response không đúng định dạng mà API Gateway chấp nhận.
API Gateway logging
Để enable log cho API Gateway, ta làm như sau:
- Truy cập API Gateway Console https://console.aws.amazon.com/apigateway/home
- Chọn books-api.
- Nhấn vào menu Stages -> chọn staging.
- Bật qua tab Logs, Tracing, bấm Enable CloudWatch Logs.
Và để API Gateway có quyền tạo log group, thì ta cần thêm quyền cho nó, cái này mình đã thêm ở trong terraform file, các bạn bấm qua mục Settings nằm ở cuối cùng sẽ thấy.
Oke, giờ ta gọi lại api staging, sau đó vào log group ta sẽ thấy một log group mới được tạo ra cho API Gateway.
Nhấn vào nó và bấm vào log stream mới nhất. Ta sẽ thấy lỗi.
Lý do là vì ta gọi hàm log.Fatal(err.Error())
, nó sẽ trả về os.Exit(1) làm API Gateway của ta tưởng là trong quá trình trigger Lambda thì Lambda bị lỗi, nên API Gateway sẽ trả về message là Internal server error. Ta sửa lại như sau.
...
if err != nil {
log.Println("err: " + err.Error())
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: err.Error(),
}, nil
}
...
Commit và push code hoặc dùng câu lệnh CLI để cập nhật Lambda, sau đó ta gọi lại api staging, ta thấy lỗi sẽ được trả về đúng.
$ curl https://x618g5ucq7.execute-api.us-west-2.amazonaws.com/staging/books ; echo
operation error DynamoDB: Scan, https response error StatusCode: 400, RequestID: D3FOENAN7GKTPENMB2V0EDJ6ENVV4KQNSO5AEMVJF66Q9ASUAAJG, api error AccessDeniedException: User: arn:aws:sts::<ACCOUNT_ID>:assumed-role/lambda_role/books_list is not authorized to perform: dynamodb:Scan on resource: arn:aws:dynamodb:us-west-2:<ACCOUNT_ID>:table/book
Oke, giờ ta đã biết cách kiểm tra log và debug cho Lambda và API Gateway 😁.
Monitoring with CloudWatch Metrics
Bên cạnh việc ghi log, nếu ta muốn xem các thông số về performance của AWS Lambda thì ta sẽ cần CloudWatch Metrics. Mặc định khi một Lambda function được thực thi, nó sẽ export cho CloudWatch một số metrics như là resource usage, execution duration, và thời gian tính tiền của Lambda.
Để xem các thông số được monitor của Lambda, ta truy cập Lambda Console, bấm vào books_list function và bấm qua Tab Monitor. Bạn sẽ thấy một số metrics hữu ích như:
- Số lần function được thực thi.
- Thời gian thực thi của mỗi lần.
- Error rates, và throttle count.
Bên cạnh các metrics có sẵn này, ta cũng có thể tạo custom metrics để phục vụ cho một mục đích nào đó của ta. Để tạo custom metrics, ta sẽ dùng CloudWatch Golang SDK. Ví dụ ta muốn tạo một metrics đại diện cho số lần Lambda kết nối tới DynamoDB mà bị lỗi, đoạn code mẫu sau sẽ được dùng để tạo CloudWatch custom metrics.
cfg, err := config.LoadDefaultConfig(context.TODO())
cw := cloudwatch.NewFromConfig(cfg)
input := &cloudwatch.PutMetricDataInput{
Namespace: aws.String("Lambda"),
MetricData: []types.MetricDatum{
{
MetricName: aws.String("FailedConnectToDynamoDB"),
Unit: types.StandardUnitCount,
Value: aws.Float64(1.0),
Dimensions: []types.Dimension{
{
Name: aws.String("env"),
Value: aws.String("staging"),
},
},
},
},
}
cw.PutMetricData(context.TODO(), input)
Ở đoạn code trên ta tạo một metrics với Namespace là Lambda, tên metrics là FailedConnectToDynamoDB, loại metrics là StandardUnitCount nghĩa là metrics này có thể dùng để đếm được, value ta để là 1.0 nghĩa là mỗi lần metrics này được put vào trong CloudWatch thì được xem như là 1 lần (có nghĩa là 1 lần kết nối thất bại).
Oke, với đoạn code trên, ta gắn nó vào trong function list như sau. Cập nhật file main.go.
package main
import (
"context"
"encoding/json"
"log"
"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/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)
type Book struct {
Id string `json:"id"`
Name string `json:"name"`
Author string `json:"author"`
Image string `json:"image"`
}
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
}
cw := cloudwatch.NewFromConfig(cfg)
svc := dynamodb.NewFromConfig(cfg)
out, err := svc.Scan(context.TODO(), &dynamodb.ScanInput{
TableName: aws.String("book"),
})
if err != nil {
log.Println("err: " + err.Error())
input := &cloudwatch.PutMetricDataInput{
Namespace: aws.String("Lambda"),
MetricData: []types.MetricDatum{
{
MetricName: aws.String("FailedConnectToDynamoDB"),
Unit: types.StandardUnitSeconds,
Value: aws.Float64(1.0),
Dimensions: []types.Dimension{
{
Name: aws.String("env"),
Value: aws.String("staging"),
},
},
},
},
}
cw.PutMetricData(context.TODO(), input)
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",
"Access-Control-Allow-Origin": "*",
},
Body: string(res),
}, nil
}
func main() {
lambda.Start(list)
}
Oke, để kiểm tra custom metric của ta, truy cập CloudWatch bấm vào menu Metrics, chọn All metrics, bạn sẽ thấy có một mục Custom namespaces và sẽ có Namespace Lambda của ta.
Lưu ý là để Lambda có thể tạo được custom metric thì ta cần cấp quyền cho nó, tất cả quyền của Lambda đều được cấp trong Terraform, nằm ở file terraform-start/policies/lambda_policy.json
.
CloudWatch Alarm
CloudWatch cho phép ta tạo một alarm để gửi thông báo theo cấu hình metrics của resource mà ta mong muốn, ví dụ trong bài này ta muốn gửi thông báo tới cho dev là khi function list của ta kết nối tới DynamoDB bị thất bại vượt quá năm lần trong 5 phút, để dev biết là đang có lỗi để lên kiểm tra code. Để tạo alarm, ta thực hiện các bước sau:
- Truy cập CloudWatch Console, mục menu chọn All alarms.
- Bấm Create alarm, qua UI tạo alarm, chọn Select metric, chọn custom metrics của ta.
- Ở mục Statistic ta chọn sum, Period ta chọn 5 minutes.
- Kéo xuống mục Conditions, ta chọn như sau.
- Bấm Next, mục Notification ta chọn In alarm, chọn Create new topic và nhập vào email của bạn (chỗ này bạn có thể tạo sns khác như bài 9 để custom chức năng gửi thông báo tới bất kì đâu cũng được). Bấm Create topic ở dưới chỗ nhập email. Sau đó đăng nhập vào email của bạn để Confirm subscription.
- Bấm Next, nhập tên của alarm.
- Bấm Next và bấm Create alarm.
Giờ bạn gọi api staging, khi nó bị lỗi quá 5 lần thì alarm của ta sẽ được trigger.
Kiểm tra email.
Oke, vậy là ta đã tìm hiểu xong cách để monitoring tài nguyên của AWS Lambda và gửi alarm khi có sự cố 😁.
Tracing with AWS X-Ray
AWS có cung cấp cho ta một dịch vụ là X-Ray dùng để track incoming và outgoing requests tới Lambda functions. Nó tổng hợp cho ta các thông tin cần thiết để giúp ta debug, analyze, và optimize function của ta.
Thông thường khi ta xây dựng REST API thì việc tracing này rất quan trọng, vì nó sẽ cho ta biết một request tới API sẽ mất bao lâu sẽ có kết quả trả về, request đi qua những gì, từng đoạn đó sẽ tốn bao nhiêu thời gian, khi ta xem những thông tin này ta có thể biết API của ta chậm ở chỗ nào và từ đó ta có thể dễ dàng optimize nó. Ví dụ minh họa của một tracing.
Để enable tracing, ta truy cập vào Lambda books_list, chọn qua tab Configuration, chọn mục Monitoring and operations tools, bấm Eidt.
Ta enable AWS X-Ray lên và bấm Save.
Sau khi active X-Ray, ta gọi lại API.
curl https://x618g5ucq7.execute-api.us-west-2.amazonaws.com/staging/books ; echo
Sau đó, để coi tracing API của ta, ta truy cập CloudWatch Console, ở mục X-Ray traces chọn menu Traces. Ta thấy trace của API ta vừa mới gọi.
Bấm vào nó và ta sẽ qua trang detail trace của API đó, nó sẽ có một Trace Map hiển thị đường đi của một request tới API.
Và Segments Timeline hiển thị thời gian thực thi của từng segment.
Ví dụ ở trên, thời gian thực thi của một API function books_list là 736ms, trong đó, thời gian function được khởi tạo là 97ms, thời gian function thực thi là 413ms.
Ta có thể tạo thêm custom segment chi tiết hơn bằng cách dùng Golang SDK. Ví dụ, ta sẽ thêm một segment nữa ở khúc Lambda function kết nối tới DynamoDB, cập nhật code 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-xray-sdk-go/instrumentation/awsv2"
"github.com/aws/aws-xray-sdk-go/xray"
)
type Book struct {
Id string `json:"id"`
Name string `json:"name"`
Author string `json:"author"`
Image string `json:"image"`
}
func list(context context.Context) (events.APIGatewayProxyResponse, error) {
ctx, root := xray.BeginSubsegment(context, "books_list")
defer root.Close(nil)
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: "Error while retrieving AWS credentials",
}, nil
}
awsv2.AWSV2Instrumentor(&cfg.APIOptions)
svc := dynamodb.NewFromConfig(cfg)
out, err := svc.Scan(ctx, &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",
"Access-Control-Allow-Origin": "*",
},
Body: string(res),
}, nil
}
func main() {
lambda.Start(list)
}
Bạn gọi lại api, reload lại trang CloudWatch X-Ray Trace, ta sẽ thấy có một trace mới, khi kiểm tra thì ta sẽ thấy có thêm segment chỗ kết nối với DynamoDB.
Segments Timeline.
Kết luận
Vậy là ta đã tìm hiểu xong về debugging , monitoring và tracing Lambda sử dụng CloudWatch và X-Ray, đây là những công cụ rất hữu ích khi ta tương tác với Lambda trên AWS, thay vì phải tự xây logging và tracing solution. 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.
Mục tìm kiếm đồng đội
Hiện tại thì bên công ty mình, là Hoàng Phúc International, với hơn 30 năm kinh nghiệm trong lĩnh vực thời trang. Và là trang thương mại điện tử về thời trang lớn nhất Việt Nam. Team công nghệ của HPI đang tìm kiếm đồng đội cho các vị trí như:
- Senior Backend Engineer (Java). Link JD: https://tuyendung.hoang-phuc.com/job/senior-backend-engineer-1022
- Senior Front-end Engineer (VueJS). https://tuyendung.hoang-phuc.com/job/senior-frontend-engineer-1021
- Junior Backend Engineer (Java). https://tuyendung.hoang-phuc.com/job/junior-backend-engineer-1067
- Junior Front-end Engineer (VueJS). https://tuyendung.hoang-phuc.com/careers/job/1068
- App (Flutter). https://tuyendung.hoang-phuc.com/job/mobile-app-engineer-flutter-1239
- Senior Data Engineer. https://tuyendung.hoang-phuc.com/job/seniorjunior-data-engineer-1221
Với mục tiêu trong vòng 5 năm tới về mảng công nghệ là:
- Sẽ có trang web nằm trong top 10 trang web nhanh nhất VN với 20 triệu lượt truy cập mỗi tháng.
- 5 triệu loyal customers và có hơn 10 triệu transactions mỗi năm.
Team đang xây dựng một hệ thống rất lớn với rất nhiều vấn để cần giải quyết, và sẽ có rất nhiều bài toàn thú vị cho các bạn. Nếu các bạn có hứng thú trong việc xây dựng một hệ thống lớn, linh hoạt, dễ dàng mở rộng, và performance cao với kiến trúc microservices thì hãy tham gia với tụi mình.
Nếu các bạn quan tâm hãy gửi CV ở trong trang tuyển dụng của Hoàng Phúc International hoặc qua email của mình nha hmquan08011996@gmail.com
. Cảm ơn các bạn đã đọc.