我经常在工作中使用Go语言AWS Lambda,尤其是开发用于与安全监控相关的基础架构(此或此或此)的后端处理。
在推进开发时,有各种各样的提示说"这很方便",但是它过于分散,因此我通过在项目之间复制来进行开发。但是,随着管理的项目数量的增加,行为也有所不同,并且技巧的数量已在一定程度上积累,因此我将其打包打包。
https://github.com/m-mizutani/golambda
我知道AWS正式提供的Powertools(Python版本,Java版本),但出于完全复制它的目的,我没有这么做。此外,并非所有Go Lambda开发人员都认为"您应该遵循这种方法!"例如,由API网关调用的Lambda可能不会从此包中受益匪浅,因为各种Web应用程序框架都支持类似的功能。因此,我希望您可以将其视为一个故事,例如"将此类处理组合在一起很方便"。
基本上,假定用于数据处理管道的Lambda和少许集成,并实现了以下4个功能。
- 检索事件
- 结构化日志
- 错误处理
- 获得隐藏的价值
实现的功能
事件检索
AWS Lambda可以指定事件源并从中触发通知。此时,通过传递事件源(例如SQS和SNS)的数据结构来启动Lambda函数。因此,有必要从各种结构数据中提取自己要使用的数据。如果在函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | package main import ( "strings" "github.com/m-mizutani/golambda" ) type MyEvent struct { Message string `json:"message"` } // SQSのメッセージをconcatして返すHandler func Handler(event golambda.Event) (interface{}, error) { var response []string // SQSのbodyを取り出す bodies, err := event.DecapSQSBody() if err != nil { return nil, err } // SQSはメッセージがバッチでうけとる場合があるので複数件とみて処理する for _, body := range bodies { var msg MyEvent // bodyの文字列をmsgにbind(中身はjson.Unmarshal) if err := body.Bind(&msg); err != nil { return nil, err } // メッセージを格納 response = append(response, msg.Message) } // concat return strings.Join(response, ":"), nil } func main() { golambda.Start(Handler) } |
此示例代码位于./example/deployable目录中,您可以在其中进行部署和试用。
与实现数据检索过程的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package main_test import ( "testing" "github.com/m-mizutani/golambda" "github.com/stretchr/testify/require" main "github.com/m-mizutani/golambda/example/decapEvent" ) func TestHandler(t *testing.T) { var event golambda.Event messages := []main.MyEvent{ { Message: "blue", }, { Message: "orange", }, } // イベントデータの埋め込み require.NoError(t, event.EncapSQS(messages)) resp, err := main.Handler(event) require.NoError(t, err) require.Equal(t, "blue:orange", resp) } |
当前,我们在SQS(订阅SNS的SQS队列)上支持SQS,SNS和SNS,但是我们计划稍后再实现DynamoDB流和Kinesis流。
结构化日志
Lambda的标准日志输出目标是CloudWatch Logs,但是Logs或Logs Viewer Insights支持JSON格式的日志。因此,拥有可以以JSON格式输出而不是使用Go语言标准
登录Lambda的要求(包括日志输出格式)通常是相同的。许多日志记录工具对于输出方法和格式具有不同的选项,但是您并不经常更改每个Lambda函数的设置。同样,在大多数情况下,只有解释消息+上下文所需的变量才足以满足输出内容的需要,因此我们在
导出全局变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // ------------ // 一時的な変数を埋め込む場合は With() を使う v1 := "say hello" golambda.Logger.With("var1", v1).Info("Hello, hello, hello") /* Output: { "level": "info", "lambda.requestID": "565389dc-c13f-4fc0-b113-xxxxxxxxxxxx", "time": "2020-12-13T02:44:30Z", "var1": "say hello", "message": "Hello, hello, hello" } */ // ------------ // request ID など、永続的に出力したい変数を埋め込む場合はSet()を使う golambda.Logger.Set("myRequestID", myRequestID) // ~~~~~~~ snip ~~~~~~ golambda.Logger.Error("oops") /* Output: { "level": "error", "lambda.requestID": "565389dc-c13f-4fc0-b113-xxxxxxxxxxxx", "time": "2020-11-12T02:44:30Z", "myRequestID": "xxxxxxxxxxxxxxxxx", "message": "oops" } */ |
另外,CloudWatch Logs编写日志的成本相对较高,如果您不断输出详细的日志,将会极大地影响成本。因此,通常仅输出最小日志较为方便,以便仅在进行故障排除或调试时才能执行详细的输出。在
错误处理
实现AWS Lambda,以便每个功能尽可能单一,并且在实现复杂的工作流时,将使用SNS,SQS,Kinesis Stream,Step Functions等将多个Lambda组合在一起。因此,如果在处理过程中发生错误,请不要尝试在Lambda代码中强行恢复,而应尽可能直接地返回错误,以使其更易于通过外部监视来发现,或者受益于Lambda自身的重试功能。会更容易接收。
另一方面,Lambda本身并不非常仔细地处理错误,因此您需要准备自己的错误处理。如前所述,配置Lambda函数很方便,这样,如果发生某种情况,它将仅返回错误并失败。因此,在大多数情况下,如果发生错误,则如果主函数(在后面描述的示例中为
由
我将详细解释每个。
详细的错误日志输出
根据经验,当发生错误时,您需要了解两个主要的调试信息:"发生的位置"和"发生的情况"。
有一些策略可以找出发生错误的位置,例如使用
您还可以通过了解引起错误的变量的内容来了解??错误的再现条件。这可以通过记录每次发生错误时可能相关的变量来解决,但是这将导致跨多个输出行的日志可见性较差(尤其是在调用较深的情况下)。另外,您必须简单地重复编写日志输出代码,这使其变得多余,并且难以简单编写,并且在进行与日志输出相关的更改时很麻烦。
因此,对于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package main import ( "github.com/m-mizutani/golambda" ) // Handler is exported for test func Handler(event golambda.Event) (interface{}, error) { trigger := "something wrong" return nil, golambda.NewError("oops").With("trigger", trigger) } func main() { golambda.Start(Handler) } |
执行此操作时,包含以下内容的日志将输出,该日志包含存储在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | { "level": "error", "lambda.requestID": "565389dc-c13f-4fc0-b113-f903909dbd45", "error.values": { "trigger": "something wrong" }, "error.stacktrace": [ { "func": "main.Handler", "file": "xxx/your/project/src/main.go", "line": 10 }, { "func": "github.com/m-mizutani/golambda.Start.func1", "file": "xxx/github.com/m-mizutani/golambda/lambda.go", "line": 127 } ], "time": "2020-12-13T02:42:48Z", "message": "oops" } |
将错误发送到错误监视服务(Sentry)
没有特别的理由使它成为Sentry,但是希望不仅对API还要对Web应用程序等Lambda函数使用某种错误监视服务。原因如下。
- 由于默认情况下无法从输出到CloudWatch Logs的日志中确定日志是正常结束还是异常结束,因此很难仅提取异常结束的执行日志。
- CloudWatch Logs没有将错误分组的功能,因此很难在100个错误中找到一个具有不同类型错误的错误。
通过设计错误日志输出方法可以在某种程度上解决这两个问题,但是建议您乖乖地使用错误监视服务,因为您必须小心并实施它。
这与输出到CloudWatch Logs的输出相同,但是由于您还可以在Sentry侧屏幕上检查它,因此"查看通知"→"查看Sentry屏幕"→"使用CloudWatch Logs搜索日志并检查详细信息您可以在第二步"是"中猜出错误。此外,对CloudWatch Logs的搜索相当轻松,因此,如果不必搜索,则更好...
顺便说一句,当您向Sentry发送错误时,Sentry事件ID会以
获取隐藏值
在Lambda中,根据执行环境而变化的参数通常存储在环境变量中并使用。如果它是个人使用的AWS账户,则将其存储在环境变量中就足够了,但是通过将秘密值和环境变量分开,Lambda信息可以将其存储在一个由多个人共享的AWS账户中或角色)只能引用秘密值,而个人(或角色)也可以引用秘密值。即使您亲自使用它,如果您处理的是真正危险的信息,在某些情况下,权限也可能分开,因此即使某些访问密钥泄漏,您也不会立即死亡。
就我而言,我经常使用AWS Secrets Manager来分隔权限 2。通过调用API从Secrets Manager检索值相对容易,但是我厌倦了编写大约100次相同的过程,因此我将其模块化。您可以通过将
1 2 3 4 5 6 7 | type mySecret struct { Token string `json:"token"` } var secret mySecret if err := golambda.GetSecretValues(os.Getenv("SECRET_ARN"), &secret); err != nil { log.Fatal("Failed: ", err) } |
功能未实现
我认为它会很有用,但是我忘了实现它。
- 在超时之前执行任意处理:Lambda将在设置的最大执行时间后静默死,因此有一种技术可以在超时之前调用某些处理以输出性能分析信息。但是,就我而言,我几乎没有Lambda函数因超时而死的经验,因此我认为这很有用,但我没有碰它。
- 跟踪:Python的Powertools提供了使用注释和更多功能来评估AWS X-Ray上的性能的功能。当我尝试使用Go进行此操作时,我没有想到一种比使用官方SDK更容易的方法,因此我没有做任何特别的事情。
概括
因此,这是我在Go中实现Lambda的最佳实践的总结,以及对编码版本的介绍。正如我在一开始所写的那样,我只是满足了我的需要,所以我认为它可以为每个人所用,但我希望它能有所帮助。
我认为有一个惯例,这些错误生成方法是
另一个选项是在AWS Systems Manager参数存储中放置一个秘密值。我个人使用的Secrets Manager具有旋转功能,例如RDS密码,因为我认为它更适合作为服务概念。但是,成本和API速率限制也不同,因此最好根据要求正确使用它们。 ?