照片由 Lukáš Vaňátko 在 Unsplash 上拍摄
Go 由于其简单性一直是我最喜欢的语言之一。最近,我决定弄清楚如何使用用 Go 编写的 lambda 函数创建一个简单的样板无服务器项目。我对工具和开发人员体验很好奇。
我想创建一个 REST API,它使用 postgres db 作为数据层。我的初步要求如下
您需要安装 Go 以及 AWS SAM。如果您将项目部署到 AWS,您可能需要为正在创建的资源付费,因此请记住在不需要资源时清除资源。
对于数据库我使用supabase
代码可在此存储库中找到
让我们从运行 sam init 开始。我选择了 Hello World 模板,Go 提供了 al.2023 env。以前有一个 Go 的托管运行时,但现在它已被弃用。
将 API 架构定义为 OpenApi 规范有一些明显的优势。我们可以用它来生成文档、创建客户端、服务器等。我还用它来定义 AWS HttpApi 网关的形状。
我的架构很简单。唯一有趣的部分是 x-amazon-apigateway-integration 属性,它允许与 lambda 集成连接。该设置与语言无关。
您可以在存储库中找到架构文件
# template.yaml # .... ItemsAPI: Type: AWS::Serverless::HttpApi Properties: StageName: Prod DefinitionBody: 'Fn::Transform': Name: 'AWS::Include' Parameters: Location: './api.yaml' FailOnWarnings: false DBSecret: Type: AWS::SecretsManager::Secret Properties: Name: my-db-secret Description: Postgres config string SecretString: 'host=172.17.0.1 port=5431 user=admin password=root dbname=lambdas sslmode=disable' # ....
如上所述,这里没有任何特定于 Go 的内容。 HttpApi Gateway是基于OpenApi创建的。
还有一个存储连接字符串的秘密。我会在部署后更新它的值
AWS SAM 对 Go 的支持非常棒。我可以将 CodeUri 指向带有 lambda 处理程序的文件夹,并将构建方法定义为 go1.x
Go 中内置的 Lambda 函数使用provided.al2023运行时,因为它们生成单个自包含的二进制文件。
函数的定义如下:
# template.yaml # .... GetItemFunction: Type: AWS::Serverless::Function Metadata: BuildMethod: go1.x Properties: Tracing: Active CodeUri: lambda_handlers/cmd/get_item/ Handler: bootstrap Runtime: provided.al2023 Architectures: - x86_64 Environment: Variables: DB_SECRET_NAME: !Ref DBSecret API_STAGE: Prod Events: HttpApiEvents: Type: HttpApi Properties: Path: /item/{id} Method: GET ApiId: !Ref ItemsAPI Policies: - AWSLambdaBasicExecutionRole - AWSSecretsManagerGetSecretValuePolicy: SecretArn: !Ref DBSecret # ....
感谢 SAM 魔法,HttpApi 网关和 lambda 函数之间的连接将使用所有必需的权限建立。
老实说,文件夹结构可能不符合习惯。但我尝试遵循一般的 Go 模式
lambda_handlers |--/api |--/cmd |--/internal |--/tools |--go.mod |--go.sum
cmd 是包含实际 lambda 处理程序的主文件夹
内部保存处理程序之间共享的代码
工具定义了项目中使用的附加工具
用于 openapi 生成器配置和生成模型的 api
lambda 处理程序的初始样板如下所示:
// ... func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { // handler logic } func main() { lambda.Start(handleRequest) }
通常,首先要问的问题是将AWS SDK客户端的初始化、数据库连接以及我们在冷启动期间要处理的其他事情放在哪里。
我们这里有选择。首先是遵循 AWS 文档示例中的模式并在 init() 函数内初始化服务。我不喜欢这种方法,因为它使得在单元测试中使用处理程序变得更加困难。
由于 lambda.Start() 方法将函数作为输入,我可以将其包装在自定义结构中,并使用我需要的服务对其进行初始化。就我而言,代码如下所示:
package main // imports ... type GetItemsService interface { GetItem(id int) (*api.Item, error) } type LambdaHandler struct { svc GetItemsService } func InitializeLambdaHandler(svc GetItemsService) *LambdaHandler { return &LambdaHandler{ svc: svc, } } func (h *LambdaHandler) HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { strId, err := helpers.GetPathParameter("id", request.PathParameters) if err != nil { return helpers.SendResponse("id is required", 400), nil } id, err := strconv.Atoi(*strId) if err != nil { return helpers.SendResponse("id must be an integer", 400), nil } log.Printf("id: %d", id) result, err := h.svc.GetItem(id) if err != nil { if err.Error() == "Item not found" { return helpers.SendResponse("Item not found", 404), nil } return helpers.SendResponse("error", 500), nil } jsonRes, err := json.Marshal(result) if err != nil { log.Printf("error marshalling response: %s", err.Error()) return helpers.SendResponse("internal server error", 500), nil } return helpers.SendResponse(string(jsonRes), 200), nil } func main() { dbSecretName := os.Getenv("DB_SECRET_NAME") log.Printf("dbSecretName: %s", dbSecretName) cfg, err := awssdkconfig.InitializeSdkConfig() if err != nil { log.Fatal(err) } secretsClient := awssdkconfig.InitializeSecretsManager(cfg) connString, err := secretsClient.GetSecret(dbSecretName) if err != nil { log.Fatal(err) } conn, err := db.InitializeDB(connString) if err != nil { log.Fatal(err) } defer conn.Close() log.Println("successfully connected to db") svc := items.InitializeItemsService(conn) handler := InitializeLambdaHandler(svc) lambda.Start(handler.HandleRequest) }
在 main() 函数(在冷启动期间运行)中,我从 Secretsmanager 获取秘密,然后初始化与数据库的连接。这两个功能都在内部文件夹中定义为通用助手,因此可以在其他处理程序中重用。最后,我的 ItemsService 使用创建的数据库连接进行初始化,并用于创建 lambda 处理程序。
HandleRequest 从路径参数中解析 ID,并调用 ItemsService 从 DB 中获取项目。
由于功能简单,没有太多业务逻辑。 ItemsServise 只是调用特定项目的数据库
package items import ( "database/sql" "errors" api "lambda_handlers/api" "log" ) type ItemsService struct { conn *sql.DB } func InitializeItemsService(conn *sql.DB) *ItemsService { return &ItemsService{ conn: conn, } } func (svc ItemsService) GetItem(id int) (*api.Item, error) { log.Printf("Getting item id %v", id) query := `SELECT * FROM items WHERE id = $1` var item api.Item err := svc.conn.QueryRow(query, id).Scan(&item.Id, &item.Name, &item.Price) log.Printf("Got item id %v", id) if err != nil { log.Printf("Error getting item %v: %v", id, err) if err == sql.ErrNoRows { return nil, errors.New("Item not found") } return nil, err } return &item, nil }
此时,我们不需要再做任何事情了。
我的目标是使用额外的工具,这些工具可以附加到项目依赖项中,因此无需依赖开发人员计算机上安装的工具。
在 Go 中,一种方法是将 oapi-codegen 保留在工具包中
//go:build tools // build tools package main import ( _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" )
并从 api_gen.go 内部调用它
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=cfg.yaml ../../api.yaml package api
这样我就可以运行 gogenerate 而无需单独安装 oapi-codegen 二进制文件。
我的构建过程需要两个步骤:从 OpenAPI 生成模型,以及构建项目本身。我让 AWS SAM 处理后者。
这是我的 Makefile
.PHONY: build local deploy generate: cd lambda_handlers && go generate ./... build: generate rm -rf .aws-sam sam build local: build sam local start-api --env-vars parameters.json deploy: build sam deploy
对我来说,本地测试 API Gateway 最简单的方法是运行 sam local start-api
由于我们的函数依赖于环境变量,因此我创建了 paramters.json 文件以将环境变量传递给 sam local
在进行无服务器开发时,有时您可能希望开始使用云资源,甚至进行本地开发。就我而言,我将立即使用机密管理器来存储数据库的连接字符串。这意味着,我需要先部署堆栈,以便我可以在本地开发中使用它。
我运行 make deploy 但现在我不检查整个部署,只是从控制台获取一个秘密名称。我还需要更新控制台中的秘密,以便它保存正确的连接字符串。
为了测试,我在supabase上创建了一个数据库,并在其中添加了一些虚拟记录
运行 make local 后我可以在本地测试 API
由于Go是一种编译语言,每次更改后我都需要重建项目并再次运行start-api。考虑到 Go 编译器惊人的速度,这没什么大不了的。
API Gateway URL是部署后在控制台打印出来的,也可以直接从AWS控制台抓取。
我调用端点,它按预期工作:
冷启动有点长,因为初始化需要约 300 毫秒,主要是因为它包括获取机密并建立与数据库的连接。但说实话,这不仅仅是一个不错的结果。
给定的项目是在 Go 中创建无服务器 REST API 的起点。它使用 OpenAPI 作为架构,使用 AWS SAM 来管理部署和本地测试。
我使用了外部 postgres 数据库和 AWS SDK 从机密管理器获取连接字符串。
还有 lambda 处理程序和项目服务的单元测试
我大部分时间都花在配置 AWS 部分上,这对于所有语言都是相同的。 Go 代码非常简单(对于这个简单的用例)。
免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。
Copyright© 2022 湘ICP备2022001581号-3