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

AWS Lambda 与 Go,初始样板

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

照片由 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]删除
最新教程 更多>
  • Python高效去除文本中HTML标签方法
    Python高效去除文本中HTML标签方法
    在Python中剥离HTML标签,以获取原始的文本表示Achieving Text-Only Extraction with Python's MLStripperTo streamline the stripping process, the Python standard librar...
    编程 发布于2025-07-10
  • 如何在Chrome中居中选择框文本?
    如何在Chrome中居中选择框文本?
    选择框的文本对齐:局部chrome-inly-ly-ly-lyly solument 您可能希望将文本中心集中在选择框中,以获取优化的原因或提高可访问性。但是,在CSS中的选择元素中手动添加一个文本 - 对属性可能无法正常工作。初始尝试 state)</option> < op...
    编程 发布于2025-07-10
  • 如何在Java字符串中有效替换多个子字符串?
    如何在Java字符串中有效替换多个子字符串?
    在java 中有效地替换多个substring,需要在需要替换一个字符串中的多个substring的情况下,很容易求助于重复应用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    编程 发布于2025-07-10
  • Java中Lambda表达式为何需要“final”或“有效final”变量?
    Java中Lambda表达式为何需要“final”或“有效final”变量?
    Lambda Expressions Require "Final" or "Effectively Final" VariablesThe error message "Variable used in lambda expression shou...
    编程 发布于2025-07-10
  • 如何使用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-07-10
  • C++成员函数指针正确传递方法
    C++成员函数指针正确传递方法
    如何将成员函数置于c 的函数时,接受成员函数指针的函数时,必须同时提供对象的指针,并提供指针和指针到函数。需要具有一定签名的功能指针。要通过成员函数,您需要同时提供对象指针(此)和成员函数指针。这可以通过修改Menubutton :: SetButton()(如下所示:[&& && && &&华)...
    编程 发布于2025-07-10
  • 为什么我会收到MySQL错误#1089:错误的前缀密钥?
    为什么我会收到MySQL错误#1089:错误的前缀密钥?
    mySQL错误#1089:错误的前缀键错误descript [#1089-不正确的前缀键在尝试在表中创建一个prefix键时会出现。前缀键旨在索引字符串列的特定前缀长度长度,可以更快地搜索这些前缀。了解prefix keys `这将在整个Movie_ID列上创建标准主键。主密钥对于唯一识别...
    编程 发布于2025-07-10
  • 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-07-10
  • 为什么HTML无法打印页码及解决方案
    为什么HTML无法打印页码及解决方案
    无法在html页面上打印页码? @page规则在@Media内部和外部都无济于事。 HTML:Customization:@page { margin: 10%; @top-center { font-family: sans-serif; font-weight: bo...
    编程 发布于2025-07-10
  • Python读取CSV文件UnicodeDecodeError终极解决方法
    Python读取CSV文件UnicodeDecodeError终极解决方法
    在试图使用已内置的CSV模块读取Python中时,CSV文件中的Unicode Decode Decode Decode Decode decode Error读取,您可能会遇到错误的错误:无法解码字节 在位置2-3中:截断\ uxxxxxxxx逃脱当CSV文件包含特殊字符或Unicode的路径逃...
    编程 发布于2025-07-10
  • PHP阵列键值异常:了解07和08的好奇情况
    PHP阵列键值异常:了解07和08的好奇情况
    PHP数组键值问题,使用07&08 在给定数月的数组中,键值07和08呈现令人困惑的行为时,就会出现一个不寻常的问题。运行print_r($月)返回意外结果:键“ 07”丢失,而键“ 08”分配给了9月的值。此问题源于PHP对领先零的解释。当一个数字带有0(例如07或08)的前缀时,PHP将其...
    编程 发布于2025-07-10
  • 如何使用替换指令在GO MOD中解析模块路径差异?
    如何使用替换指令在GO MOD中解析模块路径差异?
    在使用GO MOD时,在GO MOD 中克服模块路径差异时,可能会遇到冲突,其中3个Party Package将另一个PAXPANCE带有导入式套件之间的另一个软件包,并在导入式套件之间导入另一个软件包。如回声消息所证明的那样: go.etcd.io/bbolt [&&&&&&&&&&&&&&&&...
    编程 发布于2025-07-10
  • `console.log`显示修改后对象值异常的原因
    `console.log`显示修改后对象值异常的原因
    foo = [{id:1},{id:2},{id:3},{id:4},{id:id:5},],]; console.log('foo1',foo,foo.length); foo.splice(2,1); console.log('foo2', foo, foo....
    编程 发布于2025-07-10
  • 如何避免Go语言切片时的内存泄漏?
    如何避免Go语言切片时的内存泄漏?
    ,a [j:] ...虽然通常有效,但如果使用指针,可能会导致内存泄漏。这是因为原始的备份阵列保持完整,这意味着新切片外部指针引用的任何对象仍然可能占据内存。 copy(a [i:] 对于k,n:= len(a)-j i,len(a); k
    编程 发布于2025-07-10
  • 为什么使用固定定位时,为什么具有100%网格板柱的网格超越身体?
    为什么使用固定定位时,为什么具有100%网格板柱的网格超越身体?
    网格超过身体,用100%grid-template-columns 为什么在grid-template-colms中具有100%的显示器,当位置设置为设置的位置时,grid-template-colly修复了?问题: 考虑以下CSS和html: class =“ snippet-code”> g...
    编程 发布于2025-07-10

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

Copyright© 2022 湘ICP备2022001581号-3