”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > AWS Lambda 与 Go,初始样板

AWS Lambda 与 Go,初始样板

发布于2024-11-06
浏览:982

照片由 Lukáš Vaňátko 在 Unsplash 上拍摄

介绍

Go 由于其简单性一直是我最喜欢的语言之一。最近,我决定弄清楚如何使用用 Go 编写的 lambda 函数创建一个简单的样板无服务器项目。我对工具和开发人员体验很好奇。

目标

我想创建一个 REST API,它使用 postgres db 作为数据层。我的初步要求如下

  • 使用 OpenApi 定义的规范,以及从中生成的模型
  • 使用 AWS SAM
  • 每个端点都由单独的 lambda 函数处理
  • 本地开发尽可能简单
  • 部署相同

先决条件

您需要安装 Go 以及 AWS SAM。如果您将项目部署到 AWS,您可能需要为正在创建的资源付费,因此请记住在不需要资源时清除资源。
对于数据库我使用supabase

构建项目

代码可在此存储库中找到

让我们从运行 sam init 开始。我选择了 Hello World 模板,Go 提供了 al.2023 env。以前有一个 Go 的托管运行时,但现在它已被弃用。

OpenApi 架构

将 API 架构定义为 OpenApi 规范有一些明显的优势。我们可以用它来生成文档、创建客户端、服务器等。我还用它来定义 AWS HttpApi 网关的形状。

我的架构很简单。唯一有趣的部分是 x-amazon-apigateway-integration 属性,它允许与 lambda 集成连接。该设置与语言无关。

您可以在存储库中找到架构文件

SAM模板

HttpApi 和秘密

# 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

AWS Lambda with Go, initial boilerplate

由于Go是一种编译语言,每次更改后我都需要重建项目并再次运行start-api。考虑到 Go 编译器惊人的速度,这没什么大不了的。

在 AWS 上测试

API Gateway URL是部署后在控制台打印出来的,也可以直接从AWS控制台抓取。

我调用端点,它按预期工作:

AWS Lambda with Go, initial boilerplate

冷启动有点长,因为初始化需要约 300 毫秒,主要是因为它包括获取机密并建立与数据库的连接。但说实话,这不仅仅是一个不错的结果。

概括

给定的项目是在 Go 中创建无服务器 REST API 的起点。它使用 OpenAPI 作为架构,使用 AWS SAM 来管理部署和本地测试。

我使用了外部 postgres 数据库和 AWS SDK 从机密管理器获取连接字符串。

还有 lambda 处理程序和项目服务的单元测试

我大部分时间都花在配置 AWS 部分上,这对于所有语言都是相同的。 Go 代码非常简单(对于这个简单的用例)。

版本声明 本文转载于:https://dev.to/aws-builders/aws-lambda-with-go-initial-boilerplate-2787?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 反射动态实现Go接口用于RPC方法探索
    反射动态实现Go接口用于RPC方法探索
    在GO 使用反射来实现定义RPC式方法的界面。例如,考虑一个接口,例如:键入myService接口{ 登录(用户名,密码字符串)(sessionId int,错误错误) helloworld(sessionid int)(hi String,错误错误) } 替代方案而不是依靠反射...
    编程 发布于2025-06-08
  • Spark DataFrame添加常量列的妙招
    Spark DataFrame添加常量列的妙招
    在Spark Dataframe ,将常数列添加到Spark DataFrame,该列具有适用于所有行的任意值的Spark DataFrame,可以通过多种方式实现。使用文字值(SPARK 1.3)在尝试提供直接值时,用于此问题时,旨在为此目的的column方法可能会导致错误。 df.withCo...
    编程 发布于2025-06-08
  • 如何在无序集合中为元组实现通用哈希功能?
    如何在无序集合中为元组实现通用哈希功能?
    在未订购的集合中的元素要纠正此问题,一种方法是手动为特定元组类型定义哈希函数,例如: template template template 。 struct std :: hash { size_t operator()(std :: tuple const&tuple)const {...
    编程 发布于2025-06-08
  • 对象拟合:IE和Edge中的封面失败,如何修复?
    对象拟合:IE和Edge中的封面失败,如何修复?
    To resolve this issue, we employ a clever CSS solution that solves the problem:position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%)...
    编程 发布于2025-06-08
  • 如何使用替换指令在GO MOD中解析模块路径差异?
    如何使用替换指令在GO MOD中解析模块路径差异?
    在使用GO MOD时,在GO MOD 中克服模块路径差异时,可能会遇到冲突,其中3个Party Package将另一个PAXPANCE带有导入式套件之间的另一个软件包,并在导入式套件之间导入另一个软件包。如回声消息所证明的那样: go.etcd.io/bbolt [&&&&&&&&&&&&&&&&...
    编程 发布于2025-06-08
  • CSS可以根据任何属性值来定位HTML元素吗?
    CSS可以根据任何属性值来定位HTML元素吗?
    靶向html元素,在CSS 中使用任何属性值,在CSS中,可以基于特定属性(如下所示)基于特定属性的基于特定属性的emants目标元素: 字体家庭:康斯拉斯(Consolas); } 但是,出现一个常见的问题:元素可以根据任何属性值而定位吗?本文探讨了此主题。的目标元素有任何任何属性值,属...
    编程 发布于2025-06-08
  • HTML格式标签
    HTML格式标签
    HTML 格式化元素 **HTML Formatting is a process of formatting text for better look and feel. HTML provides us ability to format text without us...
    编程 发布于2025-06-08
  • 哪种方法更有效地用于点 - 填点检测:射线跟踪或matplotlib \的路径contains_points?
    哪种方法更有效地用于点 - 填点检测:射线跟踪或matplotlib \的路径contains_points?
    在Python Matplotlib's path.contains_points FunctionMatplotlib's path.contains_points function employs a path object to represent the polygon.它...
    编程 发布于2025-06-08
  • Async Void vs. Async Task在ASP.NET中:为什么Async Void方法有时会抛出异常?
    Async Void vs. Async Task在ASP.NET中:为什么Async Void方法有时会抛出异常?
    在ASP.NET async void void async void void void void void的设计无需返回asynchroncon而无需返回任务对象。他们在执行过程中增加未偿还操作的计数,并在完成后减少。在某些情况下,这种行为可能是有益的,例如未期望或明确预期操作结果的火灾和...
    编程 发布于2025-06-08
  • 如何使用Java.net.urlConnection和Multipart/form-data编码使用其他参数上传文件?
    如何使用Java.net.urlConnection和Multipart/form-data编码使用其他参数上传文件?
    使用http request 上传文件上传到http server,同时也提交其他参数,java.net.net.urlconnection and Multipart/form-data Encoding是普遍的。 Here's a breakdown of the process:Mu...
    编程 发布于2025-06-08
  • Python中嵌套函数与闭包的区别是什么
    Python中嵌套函数与闭包的区别是什么
    嵌套函数与python 在python中的嵌套函数不被考虑闭合,因为它们不符合以下要求:不访问局部范围scliables to incling scliables在封装范围外执行范围的局部范围。 make_printer(msg): DEF打印机(): 打印(味精) ...
    编程 发布于2025-06-08
  • 如何在Java的全屏独家模式下处理用户输入?
    如何在Java的全屏独家模式下处理用户输入?
    在Java 中,以全屏幕独立模式运行Java应用程序时,通常无法按期望的工作可能无法使用JAVA应用程序时,将用户输入在Java ProblemPassive rendering mode allows the use of KeyListener and ActionListener inter...
    编程 发布于2025-06-08
  • 为什么我会收到MySQL错误#1089:错误的前缀密钥?
    为什么我会收到MySQL错误#1089:错误的前缀密钥?
    mySQL错误#1089:错误的前缀键错误descript [#1089-不正确的前缀键在尝试在表中创建一个prefix键时会出现。前缀键旨在索引字符串列的特定前缀长度长度,以便更快地搜索这些前缀。理解prefix keys `这将在整个Movie_ID列上创建标准主键。主密钥对于唯一识别...
    编程 发布于2025-06-08
  • 找到最大计数时,如何解决mySQL中的“组函数\”错误的“无效使用”?
    找到最大计数时,如何解决mySQL中的“组函数\”错误的“无效使用”?
    如何在mySQL中使用mySql 检索最大计数,您可能会遇到一个问题,您可能会在尝试使用以下命令:理解错误正确找到由名称列分组的值的最大计数,请使用以下修改后的查询: 计数(*)为c 来自EMP1 按名称组 c desc订购 限制1 查询说明 select语句提取名称列和每个名称...
    编程 发布于2025-06-08
  • 如何在Chrome中居中选择框文本?
    如何在Chrome中居中选择框文本?
    选择框的文本对齐:局部chrome-inly-ly-ly-lyly solument 您可能希望将文本中心集中在选择框中,以获取优化的原因或提高可访问性。但是,在CSS中的选择元素中手动添加一个文本 - 对属性可能无法正常工作。初始尝试 state)</option> < op...
    编程 发布于2025-06-08

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3