Kiến thức

Serverless Series (Golang) – Bài 3 – Integrate AWS Lambda with DynamoDB for data persistence 12 tháng 03, 2022 – 118 lượt xem Golang DevOps AWS Đầu mục bài viết Bài viết liên quan Khoá học hay

Tác giả: Huỳnh Minh Quân – Người chỉ dẫn khóa học AWS: Learn AWS the Hard Way.

Xem các bài viết khác trong Series Serverless


Giới thiệu

Chào mừng bạn tới với loạt bài Serverless, trong bài trước chúng ta đã nói về cách sử dụng AWS API Gateway liên kết với AWS Lambda để xây dựng REST API theo mẫu hình Serverless. Ngoài ra, các hàm Lambda là phần mềm ko tình trạng nên chẳng thể lưu trữ dữ liệu, thành ra trong bài viết này chúng ta sẽ mày mò về thành phần thứ 3 để xây dựng mẫu hình Serverless trên môi trường AWS Cloud, đấy là DynamoDB.

Ở phần cuối của bài viết trước, chúng ta đã xây dựng REST API với API Gateway + Lambda theo hình minh họa sau.

image.png

Trong bài viết này, mình sẽ sử dụng Terraform để tạo hệ thống trên, nên chúng ta ko cần tạo từ đầu, nếu bạn muốn biết cách tạo bằng tay từng bước thì hãy đọc ở bài trước. Bước tiếp theo chúng ta cần khiến cho hệ thống trên là thêm DynamoDB để lưu trữ dữ liệu, minh họa như sau.

image.png

DynamoDB

DynamoDB là 1 cơ sở dữ liệu NoSQL được thiết kế và tăng trưởng bởi AWS. Đây là 1 trong những dịch vụ Serverless của AWS, nó có thể tự động mở mang quy mô tùy thuộc vào dữ liệu chúng ta ghi và đọc vào DB, dữ liệu có thể được lưu trữ dưới dạng mã hóa để tăng tính bảo mật, có thể tự động sao lưu và khôi phục dữ liệu.

image.png

Chúng ta sẽ coi xét 1 vài định nghĩa DynamoDB chính nhưng mà chúng ta cần hiểu trước lúc sử dụng nó với Lambda.

Kiến trúc của DynamoDB

Bao gồm Chiếc bàn là 1 nhiều mục (hàng), với mỗi mục là 1 các tính chất (cột) và trị giá.

image.png

Trong bảng sẽ có khóa chínhvà khóa chính có 2 loại:

  • Khóa phân vùng: là 1 khóa băm, trị giá của nó sẽ là ID độc nhất trong 1 bảng.
  • Khóa phân vùng + khóa sắp đặt: là 1 cặp khóa chính, với khóa phân vùng dùng để khái niệm mục đấy trong bảng và khóa sắp đặt dùng để sắp đặt mục đấy theo khóa phân vùng.

image.png

Bên cạnh đó trong bảng còn có Mục lụcgiống như các loại cơ sở dữ liệu khác nhưng mà nó sử dụng để tăng vận tốc truy hỏi của bảng, có 2 loại chỉ mục: Chỉ mục thứ cấp thế giới (GSI) và Chỉ mục thứ cấp cục bộ (LSI).

Tương tác với DynamoDB

tôi sẽ có hoạt động sau để tương tác với DynamoDB:

  • Quét: Thao tác này sẽ đi qua toàn thể bảng để tìm các mục theo các điều kiện nhất mực.
  • Truy hỏi: Thao tác này sẽ kiếm tìm các mục theo khóa chính và trả về 1 danh sách.
  • PutItem: Thao tác này được sử dụng để tạo hoặc cập nhật 1 mục.
  • GetItem: Thao tác này sẽ kiếm tìm các mục theo khóa chính và chỉ trả về kết quả trước tiên.
  • DeleteItem: Thao tác này sẽ xóa mục trong bảng dựa trên khóa chính.

Đây là những tính năng căn bản để chúng ta tương tác với DynamoDB, để thông suốt hơn về DynamoDB thì vẫn còn nhiều điều phải mày mò 😂, trong bài này chúng ta chỉ lướt qua 1 số định nghĩa căn bản để có thể làm việc với nó, mình ko biết nhiều về DynamoDB. Hiện giờ chúng ta sẽ thực hiện tạo bảng và sẽ viết đoạn mã để Lambda lưu dữ liệu vào DynamoDB.

Tạo bảng

Truy cập vào AWS Web Console, kiếm tìm DynamoDB và nhấp vào tạo bảng, bạn sẽ thấy giao diện sau, nơi Tên bảng điền vào sách, nơi Khóa phân vùng điền id.

image.png

Các trị giá còn lại bạn để mặc định.

image.png

Bấm tạo và đợi trong chốc lát bạn sẽ thấy bảng của chúng ta.

image.png

Bạn cũng có thể tạo nó với CLI sau đây cho nhanh nếu bạn ko muốn sử 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 lúc tạo bảng, chúng tôi sẽ ghi vào 1 số dữ liệu mẫu.

Ghi dữ liệu vào DynamoDB

Bấm vào bảng sách, trong phần hành động, chọn tạo mục.

image.png

Xong điền trị giá như sau và bấm tạo.

image.png

Sau lúc tạo xong, bạn kéo xuống phần Tóm lược mặt hàng, bấm Xem mặt hàng.

image.png

Chúng ta sẽ thấy mục nhưng mà chúng ta vừa tạo.

image.png

Chúng ta có thể sử dụng CLI để ghi dữ liệu vào bảng, như sau:

$ aws dynamodb put-item --table-name books --item file://vật phẩm.json

{
"id": {
    "S": "2"
},
"name": {
    "S": "Golang"
},
"author": {
    "S": "Golang"
}
}

Kết quả.

image.png

Sau lúc ghi dữ liệu mẫu vào DB, hiện thời chúng ta sẽ chuyển sang tích hợp Lambda với DynamoDB, trước tiên chúng ta sẽ viết 1 danh sách tính năng của dữ liệu trong DynamoDB.

Tích hợp Lambda với DynamoDB

Trước tiên chúng ta sẽ tạo hàm API Gateway + Lambda trước, như đã nói ở trên chúng ta sẽ sử dụng terraform, các bạn xem bài 2 để biết cách tạo thủ công nhé. Bạn tải mã nguồn tại git repo này https://github.com/hoalongnatsu/serverless-series.git, chuyển động tới folder bai-3 / terraform-start, mở tệp chế độ / lambda_policy.json.

{
"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"
    }
]
}

Nguồn khoáng sản ở đâu arn:aws:dynamodb:us-west-2:<ACCOUNT_ID>:table/books, thay thế ACCOUNT_ID bằng nick AWS của bạn. Sau đấy, chạy lệnh:

terraform init
terraform apply -auto-approve

Lúc tạo xong, bấm vào AWS Lambda và API Gateway, bạn sẽ thấy các tính năng như sau.

image.png

Cổng API

image.png

Click vào books-api và click vào phần Stages, bạn sẽ thấy URL API ngay nơi URL mời.

image.png

Ok, vậy là chúng ta đã chuẩn bị, tiếp theo chúng ta sẽ thực hiện viết mã. Tạo folder 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 tập tin build.sh phần còn lại bạn chỉnh sửa tên tệp zip tương ứng với tên folder, tỉ dụ trong folder danh sách, tệp zip build sẽ là list.zip. Trước tiên, chúng tôi sẽ viết mã cho danh sách API trước.

Danh sách với hoạt động quét

Cập nhật mã của danh sách tệp / 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)
}

Nhập mã và tải nó 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 với DynamoDB, chúng tôi sẽ sử dụng 2 gói: github.com/aws/aws-sdk-go-v2/configgithub.com/aws/aws-sdk-go-v2/service/dynamodb.

Trước tiên, chúng ta sẽ tải cấu hình mặc định lúc hàm Lambda được thực thi bằng lệnh config.LoadDefaultConfig(context.TODO()).

Sau đấy, chúng tôi sẽ khởi tạo DynamoDB với cấu hình trên bằng lệnh dynamodb.NewFromConfig(cfg). Để lấy tất cả các mục trong bảng sách, chúng tôi sử dụng lệnh quét trong đoạn mã sau.

out, err := svc.Scan(context.TODO(), &dynamodb.ScanInput{
    TableName: aws.String("books"),
})

Gọi API của chúng tôi, sao chép URL Gọi tại 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"}}]

image.png

Chúng tôi sẽ thấy rằng dữ liệu trong bảng sách của chúng tôi đã được API trả về 1 cách xác thực, thành ra chúng tôi đã kết nối hàm Lambda với DynamoDB 😁. Nhưng mà kết quả trên nó trả về ko được đẹp cho lắm, chúng tôi sẽ sửa lại để API của chúng tôi trả về kết quả với định dạng dễ sử dụng hơn. Tôi sẽ sử dụng gói github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue Để định dạng dữ liệu, hãy tải xuống gói.

go get github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue

Cập nhật tệp main.go bằng mã 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)
}

Chúng tôi sẽ định dạng lại dữ liệu được trả về với struct Book trong mã.

books := []Book{}
err = attributevalue.UnmarshalListOfMaps(out.Items, &books)

Hiện giờ lúc chúng ta gọi lại API, chúng ta sẽ thấy kết quả được trả về ở định dạng dễ sử dụng 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"}]

image.png

Nhận 1 với hoạt động GetItem

Tiếp theo, chúng tôi sẽ khai triển API nhận 1. Cập nhật tệp 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.Vật phẩm, &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)
}

Nhập mã và tải nó 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

Để tìm các mục theo khóa chính, chúng tôi sử dụng hàm GetItem với mã:

out, err := svc.GetItem(context.TODO(), &dynamodb.GetItemInput{
    TableName: aws.String("books"),
    Key: map[string]types.AttributeValue{
        "id": &types.AttributeValueMemberS{Value: req.PathParameters["id"]},
    },
})

Rà soát API.

$ curl https://utp0mbdckb.execute-api.us-west-2.amazonaws.com/staging/books/1
{"id":"1","name":"NodeJS","author":"NodeJS"}

image.png

Nếu bạn có thể in các kết quả trên, thì API nhận 1 của chúng tôi đang chạy xác thực. Nếu bạn gọi API get one với id mục ko có trong bảng, nó sẽ ko trả về 404 nhưng mà là 1 nhân vật với trị giá của mỗi tính chất 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, bạn có thể tiến hành theo cách thủ công.

Tạo với hoạt động PutItem

Tiếp theo chúng ta sẽ tiến hành tạo API, cập nhật mã vào tệp 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"),
		Vật phẩm: 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)
}

Nhập mã và tải nó 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

Để chèn dữ liệu vào DynamoDB, chúng tôi sử dụng hàm PutItem với mã:

newBook, err := svc.PutItem(context.TODO(), &dynamodb.PutItemInput{
    TableName: aws.String("books"),
    Vật phẩm: 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,
})

Rà soát API tạo của chúng tôi.

$ 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":{}}

Ok, sau đấy chúng tôi gọi lại danh sách API.

$ 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"}]

image.png

Bạn sẽ thấy rằng dữ liệu chúng tôi vừa chèn vào cơ sở dữ liệu bằng cách sử dụng API tạo ở trên đã hiện ra, thành ra API tạo của chúng tôi đang hoạt động phổ biến.

Xóa bằng thao tác DeleteItem

Tiếp theo chúng ta sẽ tiến hành xóa API, cập nhật mã vào 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)
}

Nhập mã và tải nó 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

Để xóa dữ liệu trong DynamoDB, chúng tôi sử dụng tính năng DeleteItem với mã:

out, err := svc.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{
    TableName: aws.String("books"),
    Key: map[string]types.AttributeValue{
        "id": &types.AttributeValueMemberS{Value: req.PathParameters["id"]},
    },
})

Rà soát API xóa của tôi.

$ 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"}]

Được rồi, API xóa của tôi đang hoạt động phổ biến 😁.

Kết luận

Thành ra, chúng ta đã học cách tích hợp Lambda với DynamoDB. Nếu có thắc mắc hoặc cần trả lời thêm, bạn có thể hỏi trong phần bình luận bên dưới. Hứa gặp lại tất cả các bạn trong những bài viết tiếp theo.


Thông tin thêm

Serverless Series (Golang) – Bài 3 – Integrate AWS Lambda with DynamoDB for data persistence

12 tháng 03, 2022 – 118 lượt xem

Golang DevOps AWS

Đầu mục bài viết

Bài viết liên can

Khoá học hay

#Serverless #Series #Golang #Bài #Integrate #AWS #Lambda #DynamoDB #data #persistence12 #tháng #lượt #xem #Golang #DevOps #AWS #Đầu #mục #bài #viếtBài #viết #liên #quanKhoá #học #hay
[rule_3_plain] #Serverless #Series #Golang #Bài #Integrate #AWS #Lambda #DynamoDB #data #persistence12 #tháng #lượt #xem #Golang #DevOps #AWS #Đầu #mục #bài #viếtBài #viết #liên #quanKhoá #học #hay
Tác giả: Huỳnh Minh Quân- giảng sư khóa học AWS : Learn AWS the Hard Way.
Xem các bài viết khác thuộc Series Serverless

Giới thiệu
Chào các bạn đến với series về Serverless, ở bài trước chúng ta đã nói về cách sử dụng AWS API Gateway liên kết với AWS Lambda để xây dựng REST API theo mẫu hình Serverless. Ngoài ra, Lambda functions là stateless application nên nó chẳng thể lưu trữ dữ liệu được, nên ở bài này ta sẽ mày mò về thành phần thứ 3 để xây dựng mẫu hình Serverless trên môi trường AWS Cloud, là DynamoDB.
Xong xuôi 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 để phục vụ hệ thống ở trên, nên ta ko 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 độc giả ở bài trước nhé. Bước tiếp theo ta cần khiến 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à tăng trưởng bởi AWS. Đây là 1 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 1 vài định nghĩa chính của DynamoDB nhưng mà ta cần hiểu trước lúc 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 vật phẩm 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ó 2 loại là:

Partition key: là 1 hash key, trị giá của nó sẽ là unique ID trong 1 bảng.
Partition key + sort key: là 1 cặp primary key, với partition key dùng để khái niệm vật phẩm đấy trong 1 bảng và sort key dùng để sort vật phẩm theo partition key.

Bên cạnh đó trong table còn có Index, thì giống với các loại database khác nó dùng để tăng vận tốc query của 1 table, có 2 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 thể table để kiếm tìm vật phẩm theo điều kiện nào đấy.
Query: operation này sẽ kiếm vật phẩm theo primary key và trả về 1 list.
PutItem: operation này dùng sẽ tạo mới hoặc cập nhật lại 1 vật phẩm.
GetItem: operation này sẽ kiếm vật phẩm theo primary key và chỉ trả về kết quả trước tiên.
DeleteItem: operation này sẽ xóa vật phẩm trong table dựa vào primary key.

Đây là những hàm căn bản để ta tương tác với DynamoDB, để thông suốt hơn về DynamoDB thì còn rất nhiều thứ để học 😂, ở bài này ta chỉ xem qua 1 số cái căn bản để ta có thể làm việc với nó, mình cũng ko có biết nhiều lắm về DynamoDB 😂. Giờ ta sẽ thực hiện 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 trị giá còn lại bạn để mặc định.

Bấm tạo và chờ 1 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 ko 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 lúc tạo bảng xong thì ta sẽ ghi vào 1 vài dữ liệu mẫu.
Ghi dữ liệu vào DynamoDB
Bấm vào books table, ở mục action, chọn create vật phẩm.

Xong điền vào trị giá như sau và bấm tạo.

Sau lúc tạo xong, kéo xuống mục Vật phẩm summary, bấm vào View items.

Ta sẽ thấy vật phẩm ta vừa tạo.

Ta có thể dùng CLI để ghi dữ liệu vào bảng, như sau:

$ aws dynamodb put-item –table-name books –item file://vật phẩm.json

{
“id”: {
“S”: “2”
},
“name”: {
“S”: “Golang”
},
“author”: {
“S”: “Golang”
}
}

Kết quả.

Sau lúc ghi dữ liệu mẫu vào trong DB xong, hiện thời ta sẽ chuyển sang integrate Lambda với DynamoDB, trước tiên ta sẽ viết function list dữ liệu trong DynamoDB ra.
Integrate Lambda với DynamoDB
Trước 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, chuyển động đến thư mục 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 acc AWS của bạn. Xong sau đấy, bạn chạy câu lệnh:

terraform init
terraform apply -auto-approve

Xong lúc 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 đã sẵn sàng xong, tiếp theo ta sẽ thực hiện viết code nào. Tạo folder 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 folder, tỉ dụ ở folder list thì file zip build ra sẽ là list.zip. Trước 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 2 package là github.com/aws/aws-sdk-go-v2/config và github.com/aws/aws-sdk-go-v2/service/dynamodb.
Trước tiên, ta sẽ load config mặc định lúc Lambda function được thực thi bằng câu lệnh config.LoadDefaultConfig(context.TODO()).
Sau đấy 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 thể vật phẩm 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ề xác thực, vậy là ta đã kết nối được Lambda function với DynamoDB 😁. Mà lại kết quả ở trên nó trả về ko đượ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 := []Book{}
err = attributevalue.UnmarshalListOfMaps(out.Items, &books)

Giờ lúc 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.Vật phẩm, &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 vật phẩm 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”]},
},
})

Rà soát 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 tới API get one nhưng mà với id của vật phẩm nhưng mà ko có trong bảng, thì nó sẽ ko trả về 404 nhưng mà sẽ là 1 object với trị giá 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”),
Vật phẩm: 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”),
Vật phẩm: 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,
})

Rà soát 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 đấy 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 đã hiện ra, 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”]},
},
})

Rà soát 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 đã mày mò xong cách integrate Lambda với DynamoDB. Nếu có thắc mắc hoặc cần giảng giải rõ thêm chỗ nào thì các bạn có thể hỏi dưới phần comment. Hứa gặp mọi đứa ở bài tiếp theo.
#Serverless #Series #Golang #Bài #Integrate #AWS #Lambda #DynamoDB #data #persistence12 #tháng #lượt #xem #Golang #DevOps #AWS #Đầu #mục #bài #viếtBài #viết #liên #quanKhoá #học #hay
[rule_2_plain] #Serverless #Series #Golang #Bài #Integrate #AWS #Lambda #DynamoDB #data #persistence12 #tháng #lượt #xem #Golang #DevOps #AWS #Đầu #mục #bài #viếtBài #viết #liên #quanKhoá #học #hay
[rule_2_plain] #Serverless #Series #Golang #Bài #Integrate #AWS #Lambda #DynamoDB #data #persistence12 #tháng #lượt #xem #Golang #DevOps #AWS #Đầu #mục #bài #viếtBài #viết #liên #quanKhoá #học #hay
[rule_3_plain]

#Serverless #Series #Golang #Bài #Integrate #AWS #Lambda #DynamoDB #data #persistence12 #tháng #lượt #xem #Golang #DevOps #AWS #Đầu #mục #bài #viếtBài #viết #liên #quanKhoá #học #hay
Tác giả: Huỳnh Minh Quân- giảng sư khóa học AWS : Learn AWS the Hard Way.
Xem các bài viết khác thuộc Series Serverless

Giới thiệu
Chào các bạn đến với series về Serverless, ở bài trước chúng ta đã nói về cách sử dụng AWS API Gateway liên kết với AWS Lambda để xây dựng REST API theo mẫu hình Serverless. Ngoài ra, Lambda functions là stateless application nên nó chẳng thể lưu trữ dữ liệu được, nên ở bài này ta sẽ mày mò về thành phần thứ 3 để xây dựng mẫu hình Serverless trên môi trường AWS Cloud, là DynamoDB.
Xong xuôi 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 để phục vụ hệ thống ở trên, nên ta ko 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 độc giả ở bài trước nhé. Bước tiếp theo ta cần khiến 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à tăng trưởng bởi AWS. Đây là 1 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 1 vài định nghĩa chính của DynamoDB nhưng mà ta cần hiểu trước lúc 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 vật phẩm 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ó 2 loại là:

Partition key: là 1 hash key, trị giá của nó sẽ là unique ID trong 1 bảng.
Partition key + sort key: là 1 cặp primary key, với partition key dùng để khái niệm vật phẩm đấy trong 1 bảng và sort key dùng để sort vật phẩm theo partition key.

Bên cạnh đó trong table còn có Index, thì giống với các loại database khác nó dùng để tăng vận tốc query của 1 table, có 2 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 thể table để kiếm tìm vật phẩm theo điều kiện nào đấy.
Query: operation này sẽ kiếm vật phẩm theo primary key và trả về 1 list.
PutItem: operation này dùng sẽ tạo mới hoặc cập nhật lại 1 vật phẩm.
GetItem: operation này sẽ kiếm vật phẩm theo primary key và chỉ trả về kết quả trước tiên.
DeleteItem: operation này sẽ xóa vật phẩm trong table dựa vào primary key.

Đây là những hàm căn bản để ta tương tác với DynamoDB, để thông suốt hơn về DynamoDB thì còn rất nhiều thứ để học 😂, ở bài này ta chỉ xem qua 1 số cái căn bản để ta có thể làm việc với nó, mình cũng ko có biết nhiều lắm về DynamoDB 😂. Giờ ta sẽ thực hiện 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 trị giá còn lại bạn để mặc định.

Bấm tạo và chờ 1 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 ko 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 lúc tạo bảng xong thì ta sẽ ghi vào 1 vài dữ liệu mẫu.
Ghi dữ liệu vào DynamoDB
Bấm vào books table, ở mục action, chọn create vật phẩm.

Xong điền vào trị giá như sau và bấm tạo.

Sau lúc tạo xong, kéo xuống mục Vật phẩm summary, bấm vào View items.

Ta sẽ thấy vật phẩm ta vừa tạo.

Ta có thể dùng CLI để ghi dữ liệu vào bảng, như sau:

$ aws dynamodb put-item –table-name books –item file://vật phẩm.json

{
“id”: {
“S”: “2”
},
“name”: {
“S”: “Golang”
},
“author”: {
“S”: “Golang”
}
}

Kết quả.

Sau lúc ghi dữ liệu mẫu vào trong DB xong, hiện thời ta sẽ chuyển sang integrate Lambda với DynamoDB, trước tiên ta sẽ viết function list dữ liệu trong DynamoDB ra.
Integrate Lambda với DynamoDB
Trước 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, chuyển động đến thư mục 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 acc AWS của bạn. Xong sau đấy, bạn chạy câu lệnh:

terraform init
terraform apply -auto-approve

Xong lúc 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 đã sẵn sàng xong, tiếp theo ta sẽ thực hiện viết code nào. Tạo folder 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 folder, tỉ dụ ở folder list thì file zip build ra sẽ là list.zip. Trước 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 2 package là github.com/aws/aws-sdk-go-v2/config và github.com/aws/aws-sdk-go-v2/service/dynamodb.
Trước tiên, ta sẽ load config mặc định lúc Lambda function được thực thi bằng câu lệnh config.LoadDefaultConfig(context.TODO()).
Sau đấy 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 thể vật phẩm 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ề xác thực, vậy là ta đã kết nối được Lambda function với DynamoDB 😁. Mà lại kết quả ở trên nó trả về ko đượ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 := []Book{}
err = attributevalue.UnmarshalListOfMaps(out.Items, &books)

Giờ lúc 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.Vật phẩm, &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 vật phẩm 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”]},
},
})

Rà soát 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 tới API get one nhưng mà với id của vật phẩm nhưng mà ko có trong bảng, thì nó sẽ ko trả về 404 nhưng mà sẽ là 1 object với trị giá 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”),
Vật phẩm: 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”),
Vật phẩm: 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,
})

Rà soát 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 đấy 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 đã hiện ra, 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”]},
},
})

Rà soát 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 đã mày mò xong cách integrate Lambda với DynamoDB. Nếu có thắc mắc hoặc cần giảng giải rõ thêm chỗ nào thì các bạn có thể hỏi dưới phần comment. Hứa gặp mọi đứa ở bài tiếp theo.

Related Articles

Trả lời

Email của bạn sẽ không được hiển thị công khai.

Back to top button